spring-boot-plus integrates SpringBoot+Shiro+JWT privilege management

Keywords: Java Shiro Spring Redis

SpringBoot+Shiro+JWT Privilege Management

Shiro

  • Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password, and session management.
  • With Shiro's easy-to-understand API, you can quickly and easily access any application, from the smallest mobile application to the largest network and enterprise applications.

Three core components: Subject, Security Manager and Realms.

  • Subject represents the current user's safe operation, that is, the "current operating user".
  • Security Manager: It is the core of Shiro framework, a typical Facade model. Shiro manages internal component instances through Security Manager and provides various services for security management.
  • Realm: Realm acts as a "bridge" or "connector" between Shiro and application security data. That is to say, when authenticating (login) and authorizing (access control) users, Shiro will look up users and their privilege information from Realm of application configuration.
  • ShiroBasicArchitecture

  • ShiroArchitecture

JWT

  • JSON Web Token (JWT) is the most popular cross-domain authentication solution at present.
  • JSON Web token is an open industry standard RFC 7519 method for securely expressing declarations between two parties.

JWT Data Structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II

JWT consists of three parts: Header: Header, Payload: Load, Signature: Signature

SpringBoot+Shiro+JWT

pom.xml Shiro dependency

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

pom.xml JWT dependency

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

ShiroConfig.java configuration

@Slf4j
@Configuration
public class ShiroConfig {

    /**
     * JWT Filter name
     */
    private static final String JWT_FILTER_NAME = "jwtFilter";
    /**
     * Shiro Filter name
     */
    private static final String SHIRO_FILTER_NAME = "shiroFilter";


    @Bean
    public CredentialsMatcher credentialsMatcher() {
        return new JwtCredentialsMatcher();
    }

    /**
     * JWT Data Source Verification
     *
     * @return
     */
    @Bean
    public JwtRealm jwtRealm(LoginRedisService loginRedisService) {
        JwtRealm jwtRealm = new JwtRealm(loginRedisService);
        jwtRealm.setCachingEnabled(false);
        jwtRealm.setCredentialsMatcher(credentialsMatcher());
        return jwtRealm;
    }

    /**
     * Disable 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;
    }

    /**
     * Security Manager Configuration
     *
     * @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 To configure
     *
     * @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 Path permission configuration
     *
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition(ShiroProperties shiroProperties) {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        // Get ini format configuration
        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());
            }
        }

        // Get a custom permission path configuration set
        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 Path configuration cannot be empty");
                }
                if (StringUtils.isBlank(permission)) {
                    throw new ShiroConfigException("shiro permission config permission Can not be empty");
                }

                if (StringUtils.isNotBlank(url)) {
                    chainDefinition.addPathDefinition(url, permission);
                }
                if (ArrayUtils.isNotEmpty(urls)) {
                    for (String string : urls) {
                        chainDefinition.addPathDefinition(string, permission);
                    }
                }
            }
        }
        // The last one is set to JWTFilter
        chainDefinition.addPathDefinition("/**", JWT_FILTER_NAME);

        Map<String, String> filterChainMap = chainDefinition.getFilterChainMap();
        log.debug("filterChainMap:{}", JSON.toJSONString(filterChainMap));

        return chainDefinition;
    }


    /**
     * ShiroFilter To configure
     *
     * @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 filter configuration

@Slf4j
public 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;
    }

    /**
     * Wrap JWT Token into Authentication Token
     *
     * @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 Can not be empty");
        }
        if (JwtUtil.isExpired(token)) {
            throw new AuthenticationException("JWT Token Expired,token:" + token);
        }

        // If redis secondary check is turned on, or token login is set as a single user, then token exists in redis first.
        if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) {
            boolean redisExpired = loginRedisService.exists(token);
            if (!redisExpired) {
                throw new AuthenticationException("Redis Token Non-existent,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());
    }

    /**
     * Access Failure Handling
     *
     * @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);
        // Return to 401
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // Set the response code to 401 or output the message directly
        String url = httpServletRequest.getRequestURI();
        log.error("onAccessDenied url: {}", url);
        ApiResult apiResult = ApiResult.fail(ApiCode.UNAUTHORIZED);
        HttpServletResponseUtil.printJSON(httpServletResponse, apiResult);
        return false;
    }

    /**
     * Determine whether access is allowed
     *
     * @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 Can not be empty", e);
        } catch (Exception e) {
            log.error("Access error", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /**
     * Landing Success Processing
     *
     * @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("Authentication success,token:{},url:{}", token, url);
        // Refresh token
        JwtToken jwtToken = (JwtToken) token;
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        loginService.refreshToken(jwtToken, httpServletResponse);
        return true;
    }

    /**
     * Landing Failure Handling
     *
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.error("The landing failed. token:" + token + ",error:" + e.getMessage(), e);
        return false;
    }
}

JWT Realm configuration

@Slf4j
public 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;
    }

    /**
     * Authorization Authentication, Setting Role/Privilege Information
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.debug("doGetAuthorizationInfo principalCollection...");
        // Setting Role/Permission Information
        String token = principalCollection.toString();
        // Get username
        String username = JwtUtil.getUsername(token);
        // Get access information for logged-in user roles
        LoginSysUserRedisVo loginSysUserRedisVo = loginRedisService.getLoginSysUserRedisVo(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // Setting roles
        authorizationInfo.setRoles(loginSysUserRedisVo.getRoles());
        // Setting permissions
        authorizationInfo.setStringPermissions(loginSysUserRedisVo.getPermissions());
        return authorizationInfo;
    }

    /**
     * Landing authentication
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.debug("doGetAuthenticationInfo authenticationToken...");
        // Check token
        JwtToken jwtToken = (JwtToken) authenticationToken;
        if (jwtToken == null) {
            throw new AuthenticationException("jwtToken Can not be empty");
        }
        String salt = jwtToken.getSalt();
        if (StringUtils.isBlank(salt)) {
            throw new AuthenticationException("salt Can not be empty");
        }
        return new SimpleAuthenticationInfo(
                jwtToken,
                salt,
                getName()
        );

    }

}

More configurations: https://github.com/geekidea/spring-boot-plus

application.yml configuration

############################## spring-boot-plus start ##############################
spring-boot-plus:
  ######################## Spring Shiro start ########################
  shiro:
    # shiro ini multi-line string configuration
    filter-chain-definitions: |
      /=anon
      /static/**=anon
      /templates/**=anon
    # Permission configuration
    permission-config:
        # Exclude login and login related
      - urls: /login,/logout
        permission: anon
        # Exclude static resources
      - urls: /static/**,/templates/**
        permission: anon
        # Exclude Swagger
      - urls: /docs,/swagger-ui.html, /webjars/springfox-swagger-ui/**,/swagger-resources/**,/v2/api-docs
        permission: anon
        # Exclude Spring Boot Admin
      - urls: /,/favicon.ico,/actuator/**,/instances/**,/assets/**,/sba-settings.js,/applications/**
        permission: anon
        # test
      - url: /sysUser/getPageList
        permission: anon
  ######################## Spring Shiro end ##########################

  ############################ JWT start #############################
  jwt:
    token-name: token
    secret: 666666
    issuer: spring-boot-plus
    audience: web
    # Default expiration time of 1 hour in seconds
    expire-second: 3600
    # Whether to refresh token
    refresh-token: true
    # Refresh token interval, default 10 minutes, in seconds
    refresh-token-countdown: 600
    # redis checks whether jwt token exists, optional
    redis-check: true
    # true: The same account can only be the last token to log in. false: The same account can log in multiple times.
    single-login: false
    # Salt Value Check. If you don't add custom salt value, use secret check.
    salt-check: true
  ############################ JWT end ###############################

############################### spring-boot-plus end ###############################

Redis Stores Information

Using Redis to cache JWTToken and salt values: easy authentication, token background expiration control, etc.

  • Redis secondary check and salt value check are optional
127.0.0.1:6379> keys *
1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5"
2) "login:salt:admin"
3) "login:user:admin"
4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"
  • JwtToken information stored in Redis
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

JWT

spring-boot-plus

Posted by zampu on Tue, 08 Oct 2019 11:11:53 -0700