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