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