一、概述
由于编写的一个利用springboot 开发的web项目涉及用户权限管理(实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。)和身份验证(判断一个用户是否为合法用户的处理过程)。在学习的过程中整合了shiro+jwt,本文重点旨在小结shiro整合jwt 的使用和遇到一些坑。shrio的学习可以参考文章下方链接
二、详细介绍
2.1 shiro核心结构
- Subject
Subject即主体
,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
- SecurityManager
SecurityManager即安全管理器
,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
- Authenticator
Authenticator即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
- Authorizer
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
- Realm
Realm即领域
,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
- SessionManager
sessionManager即会话管理
,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
- SessionDAO
SessionDAO即会话dao
,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
- CacheManager
CacheManager即缓存管理
,将用户权限数据存储在缓存,这样可以提高性能。
- Cryptography
Cryptography即密码管理
,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
2.2 shiro认证与授权流程
2.3 shiro整合JWT
-
项目结构(主要涉及以下1,2,3,4):
-
话不多说先放流程图:
-
JWTAuthFilter:自定义的Filter继承AuthenticatingFilter 重载判断是否允许访问的isAccessAllowed方法 ,拒绝访问的 onAccessDenied方法,登录成功失败的方法。以及跨域支持的配置
@Data @Slf4j public class JwtAuthFilter extends AuthenticatingFilter { private static final int tokenRefreshInterval = 300; @Autowired private UserService userService; /** * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { 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"); } catch (Exception e) { log.error("登录错误", e); } return allowed || super.isPermissive(mappedValue); } /** * @Description: 使用自定义的token类,提交给shiro. * @Param: [servletRequest, servletResponse] * @return: org.apache.shiro.authc.AuthenticationToken * @Date: 2021/4/30 */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String jwtToken = getAuthzHeader(servletRequest); log.info("客户端上传的token:" + jwtToken); if (StringUtils.isNotBlank(jwtToken) && !JwtUtils.isTokenExpired(jwtToken)) { log.info("token通过验证"); return new JWTToken(jwtToken); } log.info("token没有通过验证"); return null; } /** * @Description:isAccessAllowed返回false则进入该方法,此处返回错误的响应头。 * @Param: [servletRequest, servletResponse] * @return: boolean * @Date: 2021/4/30 */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletResponse httpServletResponse = WebUtils.toHttp(servletResponse); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setStatus(HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION); fillCorsHeader(WebUtils.toHttp(servletRequest), httpServletResponse); return false; } /** * @Description: 如果Shiro Login认证成功进入该方法,即登录成功,同时进行了token刷新判断。 * @Param: [token, subject, request, response] * @return: boolean * @Date: 2021/4/30 */ @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = WebUtils.toHttp(response); String newToken = null; if (token instanceof JWTToken) { JWTToken jwtToken = (JWTToken) token; User user = (User) subject.getPrincipal(); boolean shouldRefresh = shouldTokenRefresh(JwtUtils.getIssuedAt(jwtToken.getToken())); if (shouldRefresh) { newToken = userService.generateJwtToken(user); } } if (StringUtils.isNotBlank(newToken)) httpResponse.setHeader("x-auth-token", newToken); return true; } /** * @Description: shiro的login认证失败,会调用这个方法, * @Param: [token, e, request, response] * @return: boolean * @Date: 2021/4/30 */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { log.error("token失效:" + token, e.getMessage()); return super.onLoginFailure(token, e, request, response); } protected String getAuthzHeader(ServletRequest request) { HttpServletRequest httpRequest = WebUtils.toHttp(request); String header = httpRequest.getHeader("Token"); return StringUtils.removeStart(header, "Bearer "); } protected boolean shouldTokenRefresh(Date issueAt) { LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault()); return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime); } /** * @Description: 跨域支持 * @Param: [httpServletRequest, httpServletResponse] * @return: void * @Date: 2021/4/30 */ protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); } }
-
JWTShiroRealm:从数据库获取salt等用户相关信息。在doGetAuthenticationInfo方法中对于SimpleAuthenticationInfo应该是有多种构造方法的,我一开始没有配置JWTRealm时使用利用username 传入是可以的:
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.username, user.getTokenSalt(), this.getName());
但是整合JWT 自定义JWTShiroRealm却在后续Matcher中转换错误,后面利用整个user构造,就可以了 :
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getTokenSalt(), this.getName());
完整代码:
@Slf4j public class JWTShiroRealm extends AuthorizingRealm { @Autowired UserService userService; /** * @Description: 使用自定义的Matcher * @Param: [] * @return: * @Date: 2021/4/30 */ public JWTShiroRealm() { this.setCredentialsMatcher(new JWTCredentialsMatcher()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * @Description: 获取salt值给shiro有shiro进行认证。 * @Param: [authenticationToken] * @return: org.apache.shiro.authc.AuthenticationInfo * @Date: 2021/4/30 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("进入jwtrealm的认证方法"); JWTToken jwtToken = (JWTToken) authenticationToken; String token = jwtToken.getToken(); User user = new User(); log.info("进入jwtrealm的username" + JwtUtils.getUsername(token)); user.setAccount(JwtUtils.getUsername(token)); user = userService.getJwtTokenInfo(user); log.info("验证过程数据库查询的user" + user.toString()); if (user.getAccount() == null) { throw new AuthenticationException("token过期,请重新登录"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getTokenSalt(), this.getName()); return authenticationInfo; } /** * @Description: 限定此Realm只支持我们自定义的JWT Token * @Param: [token] * @return: boolean * @Date: 2021/4/30 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } }
-
JWTCredentialsMatcher:自定义匹配器比对token.
@Slf4j public class JWTCredentialsMatcher implements CredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { log.info("进入自定义Matcher"); JWTToken token = (JWTToken) authenticationToken; log.info("验证信息token:" + token); Object stored = authenticationInfo.getCredentials(); String salt = stored.toString(); log.info("验证信息:" + salt); log.info("验证信息:" + authenticationInfo.getPrincipals().getPrimaryPrincipal().toString()); User user = (User) authenticationInfo.getPrincipals().getPrimaryPrincipal(); log.info("正在验证的user" + user.toString()); try { Algorithm algorithm = Algorithm.HMAC256(salt); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", user.getAccount()) .build(); verifier.verify(token.getToken()); return true; } catch (UnsupportedEncodingException | JWTVerificationException e) { log.error("Token Error:{}", e.getMessage()); } return false; } }
-
ShiroConfig:Shiro相关配置,这里我曾经遇到一个问题,在shiroFilterFactoryBean配置权限Map,一开始使用HashMap,发现有的时候能够通过放行,有的时候由不能,测试了好久,也不得其解,知道我在网上看见别人说HashMap是无序的时候,才认识到了自己的错误,在这里使用有序的LinkedHashMap即可解决问题。
@Configuration public class ShiroConfig { /** * 注册shiro的Filter,拦截请求 */ @Bean public FilterRegistrationBean<Filter> filterRegistrationBean(DefaultWebSecurityManager securityManager) throws Exception { FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>(); filterRegistration.setFilter((Filter) shiroFilterFactoryBean(securityManager).getObject()); filterRegistration.addInitParameter("targetFilterLifecycle", "true"); filterRegistration.setAsyncSupported(true); filterRegistration.setEnabled(true); filterRegistration.setDispatcherTypes(DispatcherType.REQUEST); return filterRegistration; } /** * 初始化Authenticator */ @Bean public Authenticator authenticator() { ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证 authenticator.setRealms(Arrays.asList(jwtShiroRealm(), myShiroRealm())); //设置多个realm认证策略,一个成功即跳过其它的 authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); return authenticator; } /** * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。 * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,如果要完全禁用,要配合下面的noSessionCreation的Filter来实现 */ @Bean protected SessionStorageEvaluator sessionStorageEvaluator() { DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); return sessionStorageEvaluator; } /** * 用于用户名密码登录时认证的realm */ @Bean public CustomRealm myShiroRealm() { CustomRealm customerRealm = new CustomRealm(); //设置hashed凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置md5加密 credentialsMatcher.setHashAlgorithmName("md5"); //设置散列次数 credentialsMatcher.setHashIterations(1024); customerRealm.setCredentialsMatcher(credentialsMatcher); return customerRealm; } /** * 用于JWT token认证的realm */ @Bean("jwtRealm") public Realm jwtShiroRealm() { JWTShiroRealm myShiroRealm = new JWTShiroRealm(); // //设置hashed凭证匹配器 // HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // //设置md5加密 // credentialsMatcher.setHashAlgorithmName("md5"); // //设置散列次数 // credentialsMatcher.setHashIterations(1024); myShiroRealm.setCredentialsMatcher(new JWTCredentialsMatcher()); return myShiroRealm; } // //将自己的验证方式加入容器 // @Bean // public CustomRealm myShiroRealm() { // CustomRealm customerRealm = new CustomRealm(); // //设置hashed凭证匹配器 // HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // //设置md5加密 // credentialsMatcher.setHashAlgorithmName("md5"); // //设置散列次数 // credentialsMatcher.setHashIterations(1024); // customerRealm.setCredentialsMatcher(credentialsMatcher); // return customerRealm; // } //权限管理,配置主要是Realm的管理认证 @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealms(Arrays.asList(myShiroRealm(), jwtShiroRealm())); return securityManager; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> map = new LinkedHashMap<>(); //登出 map.put("/logout", "logout"); //特殊权限 map.put("/score/blogwork/showlist","roles[admin]"); map.put("/pair/import","roles[admin]"); map.put("/student/export","roles[admin]"); map.put("/details/import","roles[admin]"); map.put("/student/import","roles[teacher]"); map.put("/student/import","roles[teacher]"); //不需要验证 //需要perms[user:add]权限 map.put("/user/add", "perms[user:add]"); //登录页面放行 map.put("/login.html","anon"); // map.put("/student/scoreinquiry.html","anon"); map.put("/student/*","anon"); map.put("/teacher/*", "anon"); map.put("/student-page/*", "anon"); map.put("/performanceManagement/*","anon"); map.put("/assigement/*","anon"); map.put("/jobManagemant/*","anon"); //静态资源放行 // map.put("*.css", "anon"); // map.put("/*.html","anon"); map.put("*.js","anon"); map.put("/bootstrap-4.6.0-dist/**", "anon"); // map.put("/bootstrap-table/**", "anon"); map.put("/bootstrap-table/dist/*","anon"); map.put("/css/**", "anon"); map.put("/editor.md-master/**", "anon"); map.put("/editormd/**", "anon"); map.put("/jquery/**", "anon"); map.put("/js/**", "anon"); map.put("/layui/**", "anon"); //验证码放行 map.put("/captcha","anon"); //对所有用户认证 // map.put("/**", "anon"); map.put("/**", "authc"); //登录 shiroFilterFactoryBean.setLoginUrl("/login"); //首页 shiroFilterFactoryBean.setSuccessUrl("/index"); //错误页面,认证不通过跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/noauth"); LinkedHashMap<String, Filter> filtsMap = new LinkedHashMap<>(); filtsMap.put("authc", new JwtAuthFilter()); shiroFilterFactoryBean.setFilters(filtsMap); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } //注入权限管理 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } //整合shiroDialect @Bean public ShiroDialect getShiroDialect() { return new ShiroDialect(); } }
三、总结
以上内容为本人在学习和完成课设的过程中的一些感悟和笔记,总体下来Shiro +JWT 还是比较简单,灵活。后面希望能持续学习使用Spring Security后再来比较它们的异同。同时感谢网络上各位大佬的无私分享。
以下为参考文献和资料: