SpringBoot+Shiro+JWT权限管理

Shiro

  • Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
  • 使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

三个核心组件:Subject, SecurityManagerRealms.

  • Subject代表了当前用户的安全操作,即“当前操作用户”。
  • SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  • Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
  • ShiroBasicArchitecture

  • ShiroArchitecture

JWT

  • JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案
  • JSON Web令牌是一种开放的行业标准 RFC 7519方法,用于在双方之间安全地表示声明。

JWT 数据结构

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II

JWT有三部分组成:Header:头部,Payload:负载,Signature:签名

SpringBoot+Shiro+JWT

pom.xml Shiro依赖

<dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-spring-boot-starter</artifactId>    <version>1.4.1</version></dependency>

pom.xml JWT依赖

<dependency>    <groupId>com.auth0</groupId>    <artifactId>java-jwt</artifactId>    <version>3.8.3</version></dependency>

ShiroConfig.java配置

@Slf4j@Configurationpublic class ShiroConfig {    /**     * JWT过滤器名称     */    private static final String JWT_FILTER_NAME = "jwtFilter";    /**     * Shiro过滤器名称     */    private static final String SHIRO_FILTER_NAME = "shiroFilter";    @Bean    public CredentialsMatcher credentialsMatcher() {        return new JwtCredentialsMatcher();    }    /**     * JWT数据源验证     *     * @return     */    @Bean    public JwtRealm jwtRealm(LoginRedisService loginRedisService) {        JwtRealm jwtRealm = new JwtRealm(loginRedisService);        jwtRealm.setCachingEnabled(false);        jwtRealm.setCredentialsMatcher(credentialsMatcher());        return jwtRealm;    }    /**     * 禁用session     *     * @return     */    @Bean    public DefaultSessionManager sessionManager() {        DefaultSessionManager manager = new DefaultSessionManager();        manager.setSessionValidationSchedulerEnabled(false);        return manager;    }    @Bean    public SessionStorageEvaluator sessionStorageEvaluator() {        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();        sessionStorageEvaluator.setSessionStorageEnabled(false);        return sessionStorageEvaluator;    }    @Bean    public DefaultSubjectDAO subjectDAO() {        DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();        defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());        return defaultSubjectDAO;    }    /**     * 安全管理器配置     *     * @return     */    @Bean    public DefaultWebSecurityManager securityManager(LoginRedisService loginRedisService) {        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();        securityManager.setRealm(jwtRealm(loginRedisService));        securityManager.setSubjectDAO(subjectDAO());        securityManager.setSessionManager(sessionManager());        SecurityUtils.setSecurityManager(securityManager);        return securityManager;    }    /**     * ShiroFilterFactoryBean配置     *     * @param securityManager     * @param loginRedisService     * @param shiroProperties     * @param jwtProperties     * @return     */    @Bean(SHIRO_FILTER_NAME)    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,                                                         LoginService loginService,                                                         LoginRedisService loginRedisService,                                                         ShiroProperties shiroProperties,                                                         JwtProperties jwtProperties) {        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();        shiroFilterFactoryBean.setSecurityManager(securityManager);        Map<String, Filter> filterMap = new HashedMap();        filterMap.put(JWT_FILTER_NAME, new JwtFilter(loginService, loginRedisService, jwtProperties));        shiroFilterFactoryBean.setFilters(filterMap);        Map<String, String> filterChainMap = shiroFilterChainDefinition(shiroProperties).getFilterChainMap();        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);        return shiroFilterFactoryBean;    }    /**     * Shiro路径权限配置     *     * @return     */    @Bean    public ShiroFilterChainDefinition shiroFilterChainDefinition(ShiroProperties shiroProperties) {        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();        // 获取ini格式配置        String definitions = shiroProperties.getFilterChainDefinitions();        if (StringUtils.isNotBlank(definitions)) {            Map<String, String> section = IniUtil.parseIni(definitions);            log.debug("definitions:{}", JSON.toJSONString(section));            for (Map.Entry<String, String> entry : section.entrySet()) {                chainDefinition.addPathDefinition(entry.getKey(), entry.getValue());            }        }        // 获取自定义权限路径配置集合        List<ShiroPermissionConfig> permissionConfigs = shiroProperties.getPermissionConfig();        log.debug("permissionConfigs:{}", JSON.toJSONString(permissionConfigs));        if (CollectionUtils.isNotEmpty(permissionConfigs)) {            for (ShiroPermissionConfig permissionConfig : permissionConfigs) {                String url = permissionConfig.getUrl();                String[] urls = permissionConfig.getUrls();                String permission = permissionConfig.getPermission();                if (StringUtils.isBlank(url) && ArrayUtils.isEmpty(urls)) {                    throw new ShiroConfigException("shiro permission config 路径配置不能为空");                }                if (StringUtils.isBlank(permission)) {                    throw new ShiroConfigException("shiro permission config permission不能为空");                }                if (StringUtils.isNotBlank(url)) {                    chainDefinition.addPathDefinition(url, permission);                }                if (ArrayUtils.isNotEmpty(urls)) {                    for (String string : urls) {                        chainDefinition.addPathDefinition(string, permission);                    }                }            }        }        // 最后一个设置为JWTFilter        chainDefinition.addPathDefinition("/**", JWT_FILTER_NAME);        Map<String, String> filterChainMap = chainDefinition.getFilterChainMap();        log.debug("filterChainMap:{}", JSON.toJSONString(filterChainMap));        return chainDefinition;    }    /**     * ShiroFilter配置     *     * @return     */    @Bean    public FilterRegistrationBean delegatingFilterProxy() {        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();        DelegatingFilterProxy proxy = new DelegatingFilterProxy();        proxy.setTargetFilterLifecycle(true);        proxy.setTargetBeanName(SHIRO_FILTER_NAME);        filterRegistrationBean.setFilter(proxy);        filterRegistrationBean.setAsyncSupported(true);        filterRegistrationBean.setEnabled(true);        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);        return filterRegistrationBean;    }    @Bean    public Authenticator authenticator(LoginRedisService loginRedisService) {        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();        authenticator.setRealms(Arrays.asList(jwtRealm(loginRedisService)));        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());        return authenticator;    }    /**     * Enabling Shiro Annotations     *     * @return     */    @Bean    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {        return new LifecycleBeanPostProcessor();    }    /**     * depends-on lifecycleBeanPostProcessor     *     * @return     */    @Bean    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();        return defaultAdvisorAutoProxyCreator;    }    @Bean    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);        return authorizationAttributeSourceAdvisor;    }}

JWT过滤器配置

@Slf4jpublic class JwtFilter extends AuthenticatingFilter {    private LoginService loginService;    private LoginRedisService loginRedisService;    private JwtProperties jwtProperties;    public JwtFilter(LoginService loginService, LoginRedisService loginRedisService, JwtProperties jwtProperties) {        this.loginService = loginService;        this.loginRedisService = loginRedisService;        this.jwtProperties = jwtProperties;    }    /**     * 将JWT Token包装成AuthenticationToken     *     * @param servletRequest     * @param servletResponse     * @return     * @throws Exception     */    @Override    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {        String token = JwtTokenUtil.getToken();        if (StringUtils.isBlank(token)) {            throw new AuthenticationException("token不能为空");        }        if (JwtUtil.isExpired(token)) {            throw new AuthenticationException("JWT Token已过期,token:" + token);        }        // 如果开启redis二次校验,或者设置为单个用户token登陆,则先在redis中判断token是否存在        if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) {            boolean redisExpired = loginRedisService.exists(token);            if (!redisExpired) {                throw new AuthenticationException("Redis Token不存在,token:" + token);            }        }        String username = JwtUtil.getUsername(token);        String salt;        if (jwtProperties.isSaltCheck()){            salt = loginRedisService.getSalt(username);        }else{            salt = jwtProperties.getSecret();        }        return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond());    }    /**     * 访问失败处理     *     * @param request     * @param response     * @return     * @throws Exception     */    @Override    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);        // 返回401        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);        // 设置响应码为401或者直接输出消息        String url = httpServletRequest.getRequestURI();        log.error("onAccessDenied url:{}", url);        ApiResult apiResult = ApiResult.fail(ApiCode.UNAUTHORIZED);        HttpServletResponseUtil.printJSON(httpServletResponse, apiResult);        return false;    }    /**     * 判断是否允许访问     *     * @param request     * @param response     * @param mappedValue     * @return     */    @Override    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {        String url = WebUtils.toHttp(request).getRequestURI();        log.debug("isAccessAllowed url:{}", url);        if (this.isLoginRequest(request, response)) {            return true;        }        boolean allowed = false;        try {            allowed = executeLogin(request, response);        } catch (IllegalStateException e) { //not found any token            log.error("Token不能为空", e);        } catch (Exception e) {            log.error("访问错误", e);        }        return allowed || super.isPermissive(mappedValue);    }    /**     * 登陆成功处理     *     * @param token     * @param subject     * @param request     * @param response     * @return     * @throws Exception     */    @Override    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {        String url = WebUtils.toHttp(request).getRequestURI();        log.debug("鉴权成功,token:{},url:{}", token, url);        // 刷新token        JwtToken jwtToken = (JwtToken) token;        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);        loginService.refreshToken(jwtToken, httpServletResponse);        return true;    }    /**     * 登陆失败处理     *     * @param token     * @param e     * @param request     * @param response     * @return     */    @Override    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {        log.error("登陆失败,token:" + token + ",error:" + e.getMessage(), e);        return false;    }}

JWT Realm配置

@Slf4jpublic class JwtRealm extends AuthorizingRealm {    private LoginRedisService loginRedisService;    public JwtRealm(LoginRedisService loginRedisService) {        this.loginRedisService = loginRedisService;    }    @Override    public boolean supports(AuthenticationToken token) {        return token != null && token instanceof JwtToken;    }    /**     * 授权认证,设置角色/权限信息     *     * @param principalCollection     * @return     */    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {        log.debug("doGetAuthorizationInfo principalCollection...");        // 设置角色/权限信息        String token = principalCollection.toString();        // 获取username        String username = JwtUtil.getUsername(token);        // 获取登陆用户角色权限信息        LoginSysUserRedisVo loginSysUserRedisVo = loginRedisService.getLoginSysUserRedisVo(username);        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();        // 设置角色        authorizationInfo.setRoles(loginSysUserRedisVo.getRoles());        // 设置权限        authorizationInfo.setStringPermissions(loginSysUserRedisVo.getPermissions());        return authorizationInfo;    }    /**     * 登陆认证     *     * @param authenticationToken     * @return     * @throws AuthenticationException     */    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {        log.debug("doGetAuthenticationInfo authenticationToken...");        // 校验token        JwtToken jwtToken = (JwtToken) authenticationToken;        if (jwtToken == null) {            throw new AuthenticationException("jwtToken不能为空");        }        String salt = jwtToken.getSalt();        if (StringUtils.isBlank(salt)) {            throw new AuthenticationException("salt不能为空");        }        return new SimpleAuthenticationInfo(                jwtToken,                salt,                getName()        );    }}

更多配置:https://github.com/geekidea/spring-boot-plus

application.yml配置

############################## spring-boot-plus start ##############################spring-boot-plus:  ######################## Spring Shiro start ########################  shiro:    # shiro ini 多行字符串配置    filter-chain-definitions: |      /=anon      /static/**=anon      /templates/**=anon    # 权限配置    permission-config:        # 排除登陆登出相关      - urls: /login,/logout        permission: anon        # 排除静态资源      - urls: /static/**,/templates/**        permission: anon        # 排除Swagger      - urls: /docs,/swagger-ui.html, /webjars/springfox-swagger-ui/**,/swagger-resources/**,/v2/api-docs        permission: anon        # 排除SpringBootAdmin      - urls: /,/favicon.ico,/actuator/**,/instances/**,/assets/**,/sba-settings.js,/applications/**        permission: anon        # 测试      - url: /sysUser/getPageList        permission: anon  ######################## Spring Shiro end ##########################  ############################ JWT start #############################  jwt:    token-name: token    secret: 666666    issuer: spring-boot-plus    audience: web    # 默认过期时间1小时,单位:秒    expire-second: 3600    # 是否刷新token    refresh-token: true    # 刷新token的时间间隔,默认10分钟,单位:秒    refresh-token-countdown: 600    # redis校验jwt token是否存在,可选    redis-check: true    # true: 同一个账号只能是最后一次登陆token有效,false:同一个账号可多次登陆    single-login: false    # 盐值校验,如果不加自定义盐值,则使用secret校验    salt-check: true  ############################ JWT end ############################################################## spring-boot-plus end ###############################

Redis存储信息

使用Redis缓存JWTToken和盐值:方便鉴权,token后台过期控制等
  • Redis二次校验和盐值校验是可选的
127.0.0.1:6379> keys *1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5"2) "login:salt:admin"3) "login:user:admin"4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"
  • Redis存储的JwtToken信息
127.0.0.1:6379> get login:token:0f2c5d670f9f5b00201c78293304b5b5
{  "@class": "io.geekidea.springbootplus.shiro.vo.JwtTokenRedisVo",  "host": "127.0.0.1",  "username": "admin",  "salt": "f80b2eed0110a7ea5a94c35cbea1fe003d9bb450803473428b74862cceb697f8",  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJpc3MiOiJzcHJpbmctYm9vdC1wbHVzIiwiZXhwIjoxNTcwMzU3ODY1LCJpYXQiOjE1NzAzNTQyNjUsImp0aSI6IjE2MWQ1MDQxZmUwZjRmYTBhOThjYmQ0ZjRlNDI1ZGQ3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.0ExWSiniq7ThMXfqCOi9pCdonY8D1azeu78_vLNa2v0",  "createDate": [    "java.util.Date",    1570354265000  ],  "expireSecond": 3600,  "expireDate": [    "java.util.Date",    1570357865000  ]}

Reference

Shiro
  • https://shiro.apache.org/spring.html
  • https://shiro.apache.org/spring-boot.html
JWT
  • https://jwt.io/
  • https://github.com/auth0/java-jwt
spring-boot-plus
  • https://github.com/geekidea/spring-boot-plus
  • https://springboot.plus/guide/shiro-jwt.html