- Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、受權、密碼和會話管理。
- 使用Shiro的易於理解的API,您能夠快速、輕鬆地得到任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
三個核心組件:
Subject
,SecurityManager
和Realms
.html
- JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案
- JSON Web令牌是一種開放的行業標準 RFC 7519方法,用於在雙方之間安全地表示聲明。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II
JWT有三部分組成:Header
:頭部,Payload
:負載,Signature
:簽名
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.1</version> </dependency>
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
@Slf4j @Configuration public 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; } }
@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; } /** * 將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; } }
@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; } /** * 受權認證,設置角色/權限信息 * * @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() ); } }
############################## 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緩存JWTToken和鹽值:方便鑑權,token後臺過時控制等
127.0.0.1:6379> keys * 1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5" 2) "login:salt:admin" 3) "login:user:admin" 4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"
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 ] }
Shiro
JWT
spring-boot-plus