Shiro&Jwt验证
此篇基于 SpringBoot 整合 Shiro & Jwt 进行鉴权 相关代码编写及解析
首先我们创建 JwtFilter 类 继承自 BasicHttpAuthenticationFilter
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
此类是一个过滤器,后期会通过Shiro配置进去
重写4个重要的方法 其执行顺序亦是如下
1. preHandle(..) 前置处理 2. isAccessAllowed(..) 请求方法是否被允许 3. isLoginAttempt(..) 是否是登陆请求,去查看请求头里是否包含Authorization请求头 4. executeLogin(..) 执行登陆操作 其会调用getSubject(request, response).login(jwtToken)进行登陆验权
preHandle 方法
可以理解为前置处理,我们在这进行一些跨域必要设置
HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); //跨域请求会发送两次请求首次为预检请求,其方法为 OPTIONS if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response);
isAccessAllowed 方法
其实这个方法我们会手动调用 isLoginAttempt
方法及 executeLogin
方法isLoginAttempt
判断用户是否想要登陆,判断依据为请求头中是否包含 Authorization
授权信息,也就是所谓的 Token
如果有则再执行executeLogin
方法进行登陆验证操作,此方法在这里是验证Jwt
中Token
是否合法,不合法则返回401
需要重新登陆
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (this.isLoginAttempt(request, response)) { try { // 进行验证登陆JWT this.executeLogin(request, response); } catch (Exception e) { String msg = e.getMessage(); Throwable throwable = e.getCause(); if (throwable instanceof SignatureVerificationException) { // 该异常为JWT的AccessToken认证失败(Token或者密钥不正确) msg = "Token或者密钥不正确(" + throwable.getMessage() + ")"; } else if (throwable instanceof TokenExpiredException) { if (this.refreshToken(request, response)) { return true; } else { msg = "Token已过期(" + throwable.getMessage() + ")"; } } else { // 应用异常不为空 if (throwable != null) { // 获取应用异常msg msg = throwable.getMessage(); } } HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); try (PrintWriter out = httpServletResponse.getWriter()) { out.append("用户认证失败" + msg); } catch (IOException e) { //logger.error("直接返回Response信息出现IOException异常", e); } return false; } } else { // 没有携带Token HttpServletRequest httpRequest = WebUtils.toHttp(request); String httpMethod = httpRequest.getMethod(); String requestURI = httpRequest.getRequestURI(); logger.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod); HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); try (PrintWriter out = httpServletResponse.getWriter()) { out.append("用户认证失败" + msg); } catch (IOException e) { //logger.error("直接返回Response信息出现IOException异常", e); } return false; } return true; }
isLoginAttempt 方法 是否尝试登陆
@Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { String token = this.getAuthzHeader(request); return token != null; }
executeLogin 方法
执行登陆操作 其实就是对 Token
进行验证操作,这里我们需要另外一个类去处理 Token
验证 (MyRealm 类的doGetAuthenticationInfo方法)
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { String token = this.getAuthzHeader(request); //这里需要自己实现对Token验证操作 JwtToken jwtToken = new JwtToken(token); getSubject(request, response).login(jwtToken); return true; }
创建 JwtToken 类 继承自AuthenticationToken
org.apache.shiro.authc.AuthenticationToken
这里本来是存取用户名及密码的字段,现在因为是Token不存在用户名的问题所以把字段都设置成Token
public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = -634556778977L; private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
JwtConfig类的创建
这个类用于创建 Token
及解码 Token
里的信息
@ConfigurationProperties(prefix = "config.jwt") @Component public class JwtConfig { private String secret; private long expire; private String header; /** * 生成token * * @param subject * @return */ public String createToken(String subject) { Date nowDate = new Date(); //过期时间 Date expireDate = new Date(nowDate.getTime() + expire * 1000 * 60 * 60 * 24); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(subject) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 获取token中注册信息 * * @param token * @return */ public Claims getTokenClaim(String token) { try { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { return null; } } /** * 获取token 解析信息 * @param token * @return */ public String getTokenInfo(String token){ Claims claims=getTokenClaim(token); if(claims==null){ throw new RuntimeException("Token 信息异常"); } String tokenInfo=claims.getSubject(); if(StringUtils.isBlank(tokenInfo)){ throw new RuntimeException("Token 信息异常 解析值为空"); } return tokenInfo; } /** * 验证token是否过期失效 * * @param expirationTime * @return */ public boolean isTokenExpired(Date expirationTime) { return expirationTime.before(new Date()); } /** * 获取token失效时间 * * @param token * @return */ public Date getExpirationDateFromToken(String token) { return getTokenClaim(token).getExpiration(); } /** * 获取用户名从token中 */ public String getUsernameFromToken(String token) { return getTokenClaim(token).getSubject(); } /** * 获取jwt发布时间 */ public Date getIssuedAtDateFromToken(String token) { return getTokenClaim(token).getIssuedAt(); } ...set get }
配置文件信息
config.jwt.secret=abc321&%! config.jwt.expire=15 config.jwt.header=Authorization
实现自己的Realm 创建MyRealm类 继承自AuthorizingRealm
import io.jsonwebtoken.Claims; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @author zy */ @Component public class MyRealm extends AuthorizingRealm { private static Logger logger = LogManager.getLogger(MyRealm.class); /** * 这里需要实现自己的用户登陆验证信息及 数据权限相关的信息获取 */ @Resource private UserService userService; @Resource private JwtConfig jwtConfig; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("====================数据权限认证===================="); String username = jwtConfig.getTokenInfo(principals.toString()); UserInfo user = userService.getUserAndRole(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); List<Role> roles = user.getRoles(); if (roles.isEmpty()) { logger.warn("该用户 {} 没有角色,默认赋予user角色", username); Role r = new Role(); r.setRole("user"); roles.add(r); } /** * 这里是我自己实现的数据权限认证可做参考用 */ Set<String> permissionSet = new HashSet<>(roles.size() * 16); for (Role role : roles) { List<Permission> temList = role.getPermissions(); if (temList == null || temList.isEmpty()) { logger.warn("该角色 {} 没有赋予相应权限信息", role.getRole()); continue; } for (Permission tem : temList) { if (tem.getId().indexOf(":") > -1) { permissionSet.add(tem.getId()); } } } simpleAuthorizationInfo.setRoles(roles.stream().map(Role::getRole).collect(Collectors.toSet())); simpleAuthorizationInfo.setStringPermissions(permissionSet); return simpleAuthorizationInfo; } /** * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) { logger.info("====================Token认证===================="); String token = auth.getCredentials().toString(); Claims claims = jwtConfig.getTokenClaim(token); if (claims == null) { throw new AuthenticationException("解析Token异常"); } if (jwtConfig.isTokenExpired(claims.getExpiration())) { throw new AuthenticationException("Token过期"); } String username = claims.getSubject(); if (username == null || username == "") { logger.error("Token中帐号为空"); throw new AuthenticationException("Token中帐号为空"); } UserInfo user = userService.getUserByName(username); if (user == null) { throw new AuthenticationException("该帐号不存在"); } DataContextHolder.setCurrentUser(user); return new SimpleAuthenticationInfo(token, token, getName()); } }
配置Shiro 创建ShiroConfig类
LifecycleBeanPostProcessor
这个类并不一定要手动创建,手动创建可能存在一些问题。我遇见的坑就在这里。至于原因希望大家不吝赐教
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.filter.authc.AnonymousFilter; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * @author zy */ @Configuration public class ShiroConfig { @Bean("securityManager") public DefaultWebSecurityManager getManager(MyRealm myRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm); // 关闭Shiro自带的session DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); // 设置自定义Cache缓存 根据项目情况而设置 manager.setCacheManager(new CustomCacheManager()); return manager; } /** * 添加自己的过滤器,自定义url规则 */ @Bean("shiroFilter") public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, RedisTemplate<String, String> redisTemplate, JwtConfig jwtConfig) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //配置过滤器 对 『anon』不进行拦截 Map<String, Filter> filterMap = new HashMap<>(3); filterMap.put("anon", new AnonymousFilter()); filterMap.put("jwt", new JwtFilter(redisTemplate, jwtConfig)); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl("/login"); LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(16); // 配置不过滤 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/testpage", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/login/**", "anon"); filterChainDefinitionMap.put("/unauthorized/**", "anon"); // swagger filterChainDefinitionMap.put("/swagger**/**", "anon"); filterChainDefinitionMap.put("/v2/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/**", "jwt"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } // @Bean // public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { // return new LifecycleBeanPostProcessor(); // } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }