JWT简介
是一种认证机制,主要用于用户登陆鉴权,生成token。
JWT结构
一个JWT实际上就是一个字符串,它由三部分组成:头部、载荷与签名,由“.”隔开。
header.payload.signature
eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2OTUyNjczNTEsImNyZWF0ZVRpbWUiOjE2OTUxODA5NTE5NjAsImlkIjoxfQ.BNydrTifBXfxvFjV8-ofLZW1kodlNgsBqJeXMgesf2hLFR5bQ5vG0Ao0NKlUG9I2J9ksj-m3MFbefj7SVBxf0g
- header
jwt的头部实际就是对元数据的描述,是一个 json对象,格式如下:
{
"alg": "HS256",
"typ": "JWT"
}
其中:
alg属性表示签名使用的算法,默认为 HMAC SHA256(写为HS256),
typ 属性表示令牌的类型,JWT 令牌统一写为JWT。
最后生成 t o k e n 的时候,就是将 j s o n 使用 B a s e 64 U R L 算法转码即可。 \color{red}{最后生成token的时候,就是将json使用 Base64 URL 算法转码即可。} 最后生成token的时候,就是将json使用Base64URL算法转码即可。
在实际的工作用,使用的是简化版,如下图:
{"alg":"HS512"}
base64转码就是eyJhbGciOiJIUzUxMiJ9
一但确定算法,请求头每次生成的结果是相同的 \color{blue}{一但确定算法,请求头每次生成的结果是相同的} 一但确定算法,请求头每次生成的结果是相同的
- payload
载荷部分主要是传入一些非敏感的数据,也是json对象。
{"exp":1695197703,"createTime":1695111303930,"id":1}
除过需要传的一些数据外,还有七个默认的字段供选择:
iss (issuer):签发人/发行人
sub (subject):主题
aud (audience):用户
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):用于标识该 JWT
最后json进行base64编码,就生成token的第二部分内容
- signature
签名主要是由header和payload两部分的base64编码,然后通过header中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希,也就是Signature,且无法反向解密。
集成JWT
- 添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 创建jwt工具类
package com.test.commutill;
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @Description
* @Author yz
* @Date 2023/9/20 12:27
**/
@Component
public class JwtUtil {
private static final String secretKey="projectName"; //密钥一般为项目名称
private static final String subject="projectName"; //发行者一般为项目名称
private static final Long ttlMillis =7*24*60*60*1000L;//过期时间
/**
* 生成token
* @param claims
* @return
*/
public static String createJWT(Map<String, Object> claims){
JwtBuilder builder = Jwts.builder()
.setId(UUID.randomUUID().toString()) //jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
.setSubject(subject) // 发行者
.setIssuedAt(new Date()) // 发行时间
.signWith(SignatureAlgorithm.HS256, secretKey) // 签名类型 与 密钥
.setClaims(claims)
.setNotBefore(new Date()) //定义在什么时间之前,该jwt都是不可用的
.setExpiration(new Date(System.currentTimeMillis() + ttlMillis))
.compressWith(CompressionCodecs.DEFLATE);// 对载荷进行压缩
return builder.compact();
}
/**
* 从灵牌中获取用户声明的数据信息
* @param token
* @return
*/
public static Claims parseJWT(String token) {
return Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
/**
* 获取具体的用户id值
* @param token
* @return
*/
public static String getInfoByToken(String token){
Claims claims = parseJWT(token);
String useId = claims.get("userId").toString();//获取userId属性值
return useId;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = parseJWT(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* token 校验
* @param token
* @return
*/
public static Boolean validateToken(String token,String userId) {
String id = getInfoByToken(token);
return (userId.equals(id) && !isTokenExpired(token));
}
/**
* 刷新令牌
* @param token
* @return
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = parseJWT(token);
refreshedToken = createJWT(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
//测试数据
public static String generateToken() {
Map<String, Object> claims = new HashMap<>(4);
claims.put("userId", 123);
claims.put("phone", "13554345676");
return createJWT(claims);
}
public static void main(String[] args) {
String token="eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLSlOyMjSzNDW0sDQxMNNRSq0ogAiYW5oYgQRKi1OLPFOAYkbGOkoFGfl5qUpWSobGpqYmxiamZuZmSjpK-QWpeSAlYEZmilItAAAA__8.xtprUrY-zvyYvAoI2zFXfEL2tjZvPlMl_sP_YRT-0yU";
//String token1 = generateToken();
String claims = getInfoByToken(token);
System.out.println("用户声明的数据:"+claims);
}
}
- 配置拦截器
package com.test.intercepator;
import com.test.commutill.JwtUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: Intercepator
* @description: 拦截器实例
* @author: yz
* @create: 2021-12-10 14:28
* @Version 1.0
*/
@Component
public class Intercepator implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
Map<String, Object> map = new HashMap<>();
if (StringUtils.isBlank(token)) {
throw new RuntimeException("无token值");
}
String userId = request.getParameter("userId");
if (!JwtUtil.validateToken(token, userId)) {
throw new RuntimeException("token验证失败");
}
return true;
}
}
- 在webConfig中注册拦截器
package com.test.config;
import com.test.intercepator.Intercepator;
import com.test.intercepator.LocaleChangeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
/**
* @ClassName: WebConfig
* @description:
* @author: yz
* @create: 2021-12-10 14:01
* @Version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private Intercepator intercepator;
@Autowired
private LocaleChangeInterceptor localeChangeInterceptor;
//拦截器配置
/* addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截
excludePathPatterns:用于设置不需要拦截的过滤规则
拦截器主要用途:进行用户登录状态的拦截,日志的拦截等。*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(intercepator)
.addPathPatterns("/**")
.excludePathPatterns(
"/swagger-*/**",
"/doc.html",
"/v2/api-docs"
);
}
}
- 测试环节
可以写一个接口,进行postman测试。
package com.test.controller;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName: HelloWorldController
* @description: hello
* @author: yz
* @create: 2021-12-03 21:51
* @Version 1.0
*/
@RestController
@RequestMapping("/test")
@Api(tags = {"helloApi"})
public class HelloWorldController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String hello() {
return "Hello World!";
}
}
测试结果:
方式二:
- 添加依赖
<!--jwt的依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
- 配置工具类
package com.test.commutill;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @Author yz
* @Date 2023/9/20 16:41
**/
public class JwtUtil2 {
private static final String secretKey = "projectName"; //密钥一般为项目名称
private static final String subject = "projectName"; //发行者一般为项目名称
private static final Long ttlMillis = 7 * 24 * 60 * 60 * 1000L;//过期时间
/**
* 生成token
*
* @param map payload中需要放置的相关非敏感信息
* @return 返回的生成的token信息
*/
public static String getToken(Map<String, Object> map) {
//设置一个时间,作为令牌的过期时间 ,设置过期时间为7天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7);
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//遍历map集合,将信息放到payload中
map.forEach((k, v) -> {
builder.withClaim(k, v.toString()); //payload信息
});
//header信息可以省略,因为默认已经有算法和类型了,也可以在headerMap中设置算法
HashMap<String, Object> headerMap = new HashMap<>();
String token = builder.withHeader(headerMap) //header信息
.withExpiresAt(calendar.getTime()) //token过期时间
.sign(Algorithm.HMAC256(secretKey));//签名
return token;
}
/**
* 验证token DecodedJWT 为解密之后的对象 可以获取payload中添加的数据
*/
public static DecodedJWT verifyToken(String token) {
//进行token的校验,注意使用同样的算法和同样的秘钥
return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
}
/**
* 获取token中payload中的添加的参数 演示
*
* @return
*/
public static DecodedJWT getTokenPayloadParam(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
//Map<String, Claim> claims = verify.getClaims(); //获取payload中所有的参数
//verify.getClaim("userName").asString(); //通过key获取value,转成对应的类型
return verify;
}
//测试数据
public static String generateToken() {
Map<String, Object> claims = new HashMap<>(4);
claims.put("userId", 123);
claims.put("phone", "13554345676");
return getToken(claims);
}
public static void main(String[] args) throws JsonProcessingException {
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwaG9uZSI6IjEzNTU0MzQ1Njc2IiwiZXhwIjoxNjk1ODA0MzI0LCJ1c2VySWQiOiIxMjMifQ.BZeZ9iB7_o6C7i8DUhD-err5bEnYUCO63OQCqI64J2g";
DecodedJWT decodedJWT = getTokenPayloadParam(token);
System.out.println("header=" + decodedJWT.getHeader());
System.out.println("payload=" + decodedJWT.getPayload());
System.out.println("date=" + decodedJWT.getExpiresAt());//获取到过期时间
System.out.println("Claims=" + decodedJWT.getClaims());
System.out.println("userId=" + decodedJWT.getClaim("userId").asString());//获取属性值
}
}
- 配置拦截器
package com.test.intercepator;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.commutill.JwtUtil2;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: Intercepator
* @description: 拦截器实例
* @author: yz
* @create: 2021-12-10 14:28
* @Version 1.0
*/
@Component
public class Intercepator implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// String token = request.getHeader("token");
// Map<String, Object> map = new HashMap<>();
// if (StringUtils.isBlank(token)) {
// throw new RuntimeException("无token值");
// }
// String userId = request.getParameter("userId");
// if (!JwtUtil.validateToken(token, userId)) {
// throw new RuntimeException("token验证失败");
// }
// return true;
String token = request.getHeader("token");
Map<String, String> map = new HashMap<>();
try {
JwtUtil2.verifyToken(token);//调用token解析的工具类进行解析
return true; //请求放行
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "签名不一致异常");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "令牌过期异常");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "算法不匹配异常");
} catch (InvalidClaimException e) {
e.printStackTrace();
map.put("msg", "失效的payload异常");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "token无效");
}
//map异常的数据要返回给客户端需要转换成json格式 @ResponseBody 内置了jackson
String resultJson = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(resultJson);
return true; //异常不放行
}
}
- 注册拦截器
package com.test.config;
import com.test.intercepator.Intercepator;
import com.test.intercepator.LocaleChangeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
/**
* @ClassName: WebConfig
* @description:
* @author: yz
* @create: 2021-12-10 14:01
* @Version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private Intercepator intercepator;
@Autowired
private LocaleChangeInterceptor localeChangeInterceptor;
//拦截器配置
/* addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截
excludePathPatterns:用于设置不需要拦截的过滤规则
拦截器主要用途:进行用户登录状态的拦截,日志的拦截等。*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(intercepator)
.addPathPatterns("/**")
.excludePathPatterns(
"/swagger-*/**",
"/doc.html",
"/v2/api-docs"
);
}
}
- 验证
修改token值,点击发送按钮。弹出异常信息。
遇到的问题: