spring boot2整合shiro安全框架实现前后端分离的JWT token登录验证
代码略多,粘贴一些关键的代码,完整demo当然必须放在GitHub上面啦,当然带SQL文件的,在项目里面
GitHub地址: https://github.com/zhang-xiaoxiang/shiro-jwt
说明:由于初衷是解决自己项目的bug的,就找的网上的一面博客瞎搞了一个demo.然后报的错网上难以找到解决办法,后来自己解决了,就记录一下,所以不算教程,我看评论大伙说的修改密码后之前的token仍然有效,可以使用redis处理,或者其他业务逻辑代码处理一下,这个仅仅是一个demo,当然很多培训机构的源码也有一些demo,大家可以参考,这里主要是给需要的小伙伴一个思路或者遇到那个bug的解决方案
关于这个例子介绍:
1使用的是spring boot2作为环境基础
2数据库框架是mybatis plus(对mybatis熟悉就行,你顺便看一下mybatis plus逆向生成代码的魅力)
3包含全局异常的处理(报错getWriter() has already been called for this response),这个BUG是网上的访问量比较高的CSDN的博客案例留下的bug,为了解决这个bug才有我的这篇博客,所以记录一下
4shiro可以实现除了验证和授权之外的一些功能,这里主要展示验证,验证相当于登录,授权相当于登录后查看是否有权限,就想即使登录了支付宝,但是付款的时候还是需要验证支付宝密码一样,当然前后端未分离可以使用thymeleaf网页引擎,貌似黑马视频有,这里就只做个登录例子
5jwt这种方式是不太时尚的,正确的方式spring security oauth2实现认证授权,这里仅供参考,欢迎小伙伴提出宝贵意见,我都会尽快回复
6使用的是mysql8,pom和properties文件你改成现在用得多一点的5.7版本的写法就行了
7这里根据博友评论确实忘了用户修改密码token的处理,我相信这个大家应该会处理的(更新一下token就行了)
主要步骤:
首先是pom引入(主要的依赖,由于代码较大,而且我提供了Git地址,需要的直接从GitHub拉下来就行了,后面不做过多阐述)
实现shiro的AuthenticationToken接口的类JwtToken
//package com.example.shirojwt.jwt;import org.apache.shiro.authc.AuthenticationToken;/*** JwtToken:实现shiro的AuthenticationToken接口的类JwtToken** @author zhangxiaoxiang* @date: 2019/07/12*/
public class JwtToken implements AuthenticationToken{private String token;public JwtToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}
再写一个token的验证工具类
//package com.example.shirojwt.util;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;import java.util.Date;/*** JwtUtil:用来进行签名和效验Token** @author zhangxiaoxiang* @date: 2019/07/12*/
@Slf4j
public class JwtUtil {/*** JWT验证过期时间 EXPIRE_TIME 分钟*/private static final long EXPIRE_TIME = 30 * 60 * 1000;/*** 校验token是否正确** @param token 密钥* @param secret 用户的密码* @return 是否正确*/public static boolean verify(String token, String username, String secret) {try {//根据密码生成JWT效验器Algorithm algorithm = Algorithm.HMAC256(secret);JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();//效验TOKENDecodedJWT jwt = verifier.verify(token);log.info("登录验证成功!");return true;} catch (Exception exception) {log.error("JwtUtil登录验证失败!");return false;}}/*** 获得token中的信息无需secret解密也能获得** @return token中包含的用户名*/public static String getUsername(String token) {try {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("username").asString();} catch (JWTDecodeException e) {return null;}}/*** 生成token签名EXPIRE_TIME 分钟后过期** @param username 用户名(电话号码)* @param secret 用户的密码* @return 加密的token*/public static String sign(String username, String secret) {Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);Algorithm algorithm = Algorithm.HMAC256(secret);// 附带username信息return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);}public static void main(String[] args) {/*** 测试生成一个token*/String sign = sign("18888888888", "123456");log.warn("测试生成一个token\n"+sign);}
}
写一个jwt过滤器来作为shiro的过滤器。
//package com.example.shirojwt.filter;import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.result.ResponseData;
import com.example.shirojwt.result.ResponseDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** JwtFilter:jwt过滤器来作为shiro的过滤器** @author zhangxiaoxiang* @date: 2019/07/12*/
@Slf4j
@Component//这个注入与否影响不大
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {/*** 执行登录* @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = (HttpServletRequest) request;String token = httpServletRequest.getHeader("Token");JwtToken jwtToken = new JwtToken(token);// 提交给realm进行登入,如果错误他会抛出异常并被捕获try {getSubject(request, response).login(jwtToken);// 如果没有抛出异常则代表登入成功,返回truereturn true;} catch (AuthenticationException e) {ResponseData responseData = ResponseDataUtil.authorizationFailed( "没有访问权限,原因是:" + e.getMessage());//SerializerFeature.WriteMapNullValue为了null属性也输出json的键值对Object o = JSONObject.toJSONString(responseData, SerializerFeature.WriteMapNullValue);response.setCharacterEncoding("utf-8");response.getWriter().print(o);return false;}}/*** 执行登录认证** @param request* @param response* @param mappedValue* @return*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {try {return executeLogin(request, response);// return true;有一篇博客这里直接返回true是不正确的,在这里我特别指出一下} catch (Exception e) {log.error("JwtFilter过滤验证失败!");return false;}}/*** 对跨域提供支持* @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {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"));// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(HttpStatus.OK.value());return false;}return super.preHandle(request, response);}
}
自定义Realm
//package com.example.shirojwt.shiro;import com.example.shirojwt.entity.User;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.service.UserService;
import com.example.shirojwt.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;/*** MyRealm:自定义一个授权** @author zhangxiaoxiang* @date: 2019/07/12*/@Component
@Slf4j
public class MyRealm extends AuthorizingRealm {@Autowiredprivate UserService userService;/*** 必须重写此方法,不然Shiro会报错* @param token* @return*/@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}/*** 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的* @param principals* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = JwtUtil.getUsername(principals.toString());User user = userService.findByUserName(username);SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();return simpleAuthorizationInfo;}/*** 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。* @param auth* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {String token = (String) auth.getCredentials();// 解密获得username,用于和数据库进行对比String username = null;try {//这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些)username = JwtUtil.getUsername(token);} catch (Exception e) {throw new AuthenticationException("heard的token拼写错误或者值为空");}if (username == null) {log.error("token无效(空''或者null都不行!)");throw new AuthenticationException("token无效");}User userBean = userService.findByUserName(username);if (userBean == null) {log.error("用户不存在!)");throw new AuthenticationException("用户不存在!");}if (!JwtUtil.verify(token, username, userBean.getUserPassword())) {log.error("用户名或密码错误(token无效或者与登录者不匹配)!)");throw new AuthenticationException("用户名或密码错误(token无效或者与登录者不匹配)!");}return new SimpleAuthenticationInfo(token, token, "my_realm");}
}
ShiroConfig核心配置
//package com.example.shirojwt.config;import com.example.shirojwt.filter.JwtFilter;
import com.example.shirojwt.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;/*** ShiroConfig:shiro 配置类,配置哪些拦截,哪些不拦截,哪些授权等等各种配置都在这里* * 很多都是老套路,按照这个套路配置就行了** @author zhangxiaoxiang* @date: 2019/07/12*/@Configuration
public class ShiroConfig {/*** 注入安全过滤器* @param securityManager* @return*/@Bean("shiroFilter")public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);//拦截器Map filterChainDefinitionMap = new LinkedHashMap();// 配置不会被拦截的链接 顺序判断filterChainDefinitionMap.put("/login/**", "anon");filterChainDefinitionMap.put("/**.js", "anon");filterChainDefinitionMap.put("/druid/**", "anon");filterChainDefinitionMap.put("/swagger**/**", "anon");filterChainDefinitionMap.put("/**/swagger**/**", "anon");filterChainDefinitionMap.put("/webjars/**", "anon");filterChainDefinitionMap.put("/v2/**", "anon");// 添加自己的过滤器并且取名为jwtMap filterMap = new HashMap(1);filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);//
