JWT 的原理
基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。
token应用流程为:
1、初次登录:用户初次登录,输入用户名密码。
2、密码验证:服务器从数据库取出用户名和密码进行验证。
3、生成JWT:服务器端验证通过,根据从数据库返回的信息,以及预设规则,生成JWT。
4、返还JWT:服务器的HTTP RESPONSE中将JWT返还。
5、带JWT的请求:以后客户端发起请求,HTTP REQUEST HEADER中的Authorization字段都要有值,为JWT,用来验证用户身份以及对路由,服务和资源的访问权限进行验证。请求验证的url可以例如:http://127.0.0.1:8083/change/goodsMenu? token=JWT
JWT 是jsonWebToken 简称,现在流行的前后端分离开发(jwt不止是运用于web应用,app,小程序,H5混合开发都可)基本上都使用jwt来验证用户token,减少了服务器session 的使用,也能减轻服务器的压力,是当下Java开发不二选择
更多的关于jwt的东西都可自行百度,这里就不多赘述
使用JWT
1、添加依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
2、找到一个合适的Jwt加密工具
网上的加密工具都不一样,但达到的目的都是一样的
@Component public class JwtTokenUtil { private static final long serialVersionUID = -3301605591108950415L; private String secret = ParamUtil.getValue("jwt_secret"); private Long expiration = Long.valueOf(ParamUtil.getValue("jwt_expiration")); private String tokenHeader = ParamUtil.getValue("jwt_tokenHeader"); private Clock clock = DefaultClock.INSTANCE; public String generateToken(UserInfo userInfo) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userInfo.getName()); } public String generateToken(UserInfo user, String userName) { Map<String, Object> claims = new HashMap<>(); // 自定义参数,把需要存入token的参数都可以自定义到这里,比如 角色 部门类的关键参数 claims.put("userId", user.getId()); claims.put("phone", user.getName()); return doGenerateToken(claims, userName); } private String doGenerateToken(Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) // 存入自定义参数 .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } public Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } }
3、生产加密token,登录时返回
加密字符串可以得到了,接下来就是运用起来,在登录接口利用jwtTokenUtil 生成一个Token 并在登录接口一并返回
4、拦截器拦截并验证token
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private JwtTokenUtil jwtTokenUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果不是映射到方法直接通过 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); String servletPath = request.getServletPath(); if (methodAnnotation != null) { // 判断是否存在令牌信息,如果存在,则允许登录 //token_access 这就是前端放在请求头中的jwtToken String accessToken = request.getHeader("token_access"); if (ToolUtil.isEmpty(accessToken)) { //这里抛给前端的为一个自定义异常枚举,可自己定义 //RRException 为继承 RuntimeException 的自定义枚举 throw new RRException(RongRunErrorCodeEnum.SYSTEM_LOGIN); } try { Claims claims = jwtTokenUtil.getAllClaimsFromToken(accessToken); //通过jwt加密工具中自定义参数取出自己想要验证的值,来进行操作 Long userId = Long.valueOf(claims.get("userId").toString()); //这个可要可不要,通过这里自己在请求中加参数,可以让前端少传几个已经在token中自定义好的值 request.setAttribute("userId", userId); //接下来可处理需要验证并且已经通过后的业务 } catch (ExpiredJwtException e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token已过期"); ServletOutputStream out = response.getOutputStream(); OutputStreamWriter ow = new OutputStreamWriter(out, "utf-8"); ow.write(JSONObject.toJSONString(ApiJson.returnNG("请登录!"))); ow.flush(); ow.close(); return false; } catch (MalformedJwtException e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token有误"); ServletOutputStream out = response.getOutputStream(); OutputStreamWriter ow = new OutputStreamWriter(out, "utf-8"); ow.write(JSONObject.toJSONString(ApiJson.returnNG("请登录!"))); ow.flush(); ow.close(); return false; } return true; } }
处理类已经完成,接下来是添加拦截器 自定义类并且实现WebMvcConfigurer
@Configuration @Slf4j public class MyInterceptor implements WebMvcConfigurer { /* @Autowired private LoginInterceptor loginInterceptor;*/ /** * 重写添加拦截器方法并添加配置拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor());//添加拦截规则 } @Bean public LoginInterceptor loginInterceptor (){ return new LoginInterceptor(); } }
到此就可以完成spring boot 对jwt的实现,也可以实现用户单点登录,这里就不细说。原理就是在用户登录时就在服务器中存入token,在拦截器中获取到并对比即可
* 另外说明,以上时全局拦截,如果有的同志只需要进行局部拦截的话,可以使用自定义注解的方式
Spring boot 中使用注解也时非常方便 如果有同志对注解不是很了解的话,可以百度一哈
1、创建注解类
2、使用注解
3、在拦截规则中使用注解
Method method = handlerMethod.getMethod(); // 通过获取注解的方法,判断有注解的方法则需要拦截验证 YourAnnotation yourAnnotation = method.getAnnotation(YourAnnotation.class); // 有 @methodAnnotation 注解,需要认证 if (yourAnnotation != null) { // 判断是否存在令牌信息,如果存在,则允许登录 String accessToken = request.getHeader("token_access"); if (ToolUtil.isEmpty(accessToken)) { throw new RRException(RongRunErrorCodeEnum.SYSTEM_LOGIN); } try { Claims claims = jwtTokenUtil.getAllClaimsFromToken(accessToken); } catch (ExpiredJwtException e) { } }
这样就可以只验证加了自定义注解的接口,实现局部拦截