一、简介
1、概述
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑和自包含的方式,并且是数据结构是 JSON 格式的,所以可以在各种语言中使用。并且 JWT 是可信任和验证的,因为它里面包含了通过加密之后的签名,可以保证在传输的过程中不会被篡改。
JWT 由 Hander、PayLoad、Siganture 三部分组成,其中Siganture 是通过加密Hander 和 PayLoad 形成的签名,签名可以验证数据的完整性,防止被篡改。但需要注意的是,签名仅能保证传输的信息不被篡改,而不能保证信息传输的安全。
官网地址:https://jwt.io/introduction
2、作用
- 授权:用户登录后可以根据用户的信息构建 token,从而实现访问一些服务和资源
- 信息传输:因为 JWT 包含签名,所以可以保证传输的信息不被篡改
3、结构
JWT 的结构是以 . 进行分割的三部分组成:
- Hander
- PayLoad
- Siganture
类似于:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc4Nzc1MTQ3MX0.KcH3xxUSrbf5g36ZlJqQa2CzNRtZoNAtLLgO3LjPf5twE-K25SQCCMbeMU_4f7VMEkY-raJPcdmtdG4kztiDyA
并且我们可以通过官网进行解析:
我们可以看到上图的 jwt 标了 3 种颜色,红色、紫色和蓝色,也就是Header,PayLoad 和Siganture。其中 Header 和 PayLoad 都是由 Base 64 加密而来的,可以进行解密,只有Siganture 需要通过密钥进行加密。
- Header 部分主要是标明加密的方式,当然我们也可以自定义其中的内容
- PayLoad 部分主要是存放信息,例如用户的 id 和用户名等等,需要注意的是,由于仅通过 Base 64 加密,所以是可以解密的,因此不能在这里放用户的一些例如手机号、身份证号或者密码等敏感信息
- Siganture 部分就是签名了,上图中我们可以把密钥填进去验证数据有没有被篡改
我们在使用中一般是用于用户登录生成 token 的,我们也可以看到很多网站在请求接口时都会在请求头中看到:
Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc4Nzc1MTQ3MX0.KcH3xxUSrbf5g36ZlJqQa2CzNRtZoNAtLLgO3LjPf5twE-K25SQCCMbeMU_4f7VMEkY-raJPcdmtdG4kztiDyA
这种类型,其格式为Authorization: Bearer ,注意 Bearer 和 token 中间有一个空格
4、为什么使用 JWT
- 在传统的 session 中,每次用户登录后,服务端都需要将 session 保存在内存中,这样一旦用户增多之后,就会导致内存开销过大,或者服务重启后,会导致用户登录失效。
- 用户登录之后,也只能在这台服务器上进行资源请求,因为 session 仅保存在这台服务器中。
- 因为 session 是通过 cookie 传输的,因此如果 cookie 被截获,则很有可能会遭受 csrf。
如果使用 JWT,则不会有上面的这些问题,因为 JWT 的鉴权机制也类似于 HTTP 的无状态,不需要服务端存储认证信息,仅需验证签名即可保证信息的安全性。
5、JWT 的优点
- JWT 的长度很短,进行传输时,数据量小传输快
- PayLoad 中包含了用户的信息,因此也不需要频繁的去查询数据库
- JSON 解析器在绝大部分语言上都支持,可以很容易的解析
- 不需要在服务端存数据,特别适用于分布式微服务
二、工作流程
工作流程如图所示:
- 用户输入账号密码或者短信登录,服务器验证通过后根据用户的基本信息生成 JWT,然后将 JWT 通过响应返回给前端
- 前端拿到 token 后,通过 LocalStorage 等方式将 token 保存下来
- 当前端请求后端 API 时,在请求头中通过Authorization: Bearer 带上 token
- 服务端接收到请求后,拦截请求并查找 token 和验证 token,验证通过后将数据返回给前端;若不通过,则返回响应码 401,前端接收到响应码后跳转登录页面
三、使用
1、基本使用
- 导入 jjwt 依赖,jjwt 是 jwt 对 Java 的支持框架
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 测试代码
public class JwtTest {
public static void main(String[] args) {
// Header
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS512");
// Payload
Map<String, Object> payload = new HashMap<>();
payload.put("sub", "admin");
// 声明Token失效时间,1小时候失效
Date expire = new Date(System.currentTimeMillis() + 3600 * 1000);
// 生成Token
String token = Jwts.builder()
// 设置Header
.setHeader(header)
// 设置Payload
.setClaims(payload)
// 设置生效时间
.setExpiration(expire)
// 签名,这里的秘钥最好填复杂一点,千万不要泄露
.signWith(SignatureAlgorithm.HS512, "symx.club")
.compact();
System.out.println(token);
}
}
- 运行结果
4.官网解码
5.Java 代码解码
public class JwtTest {
public static void main(String[] args) {
// 生成的token
String token = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcwMTM1ODQxNn0.xHCSMJDNGc8jYYBva-N-s3xTMEzkul5znK9eSMbWdM0m8AleiqWqNSHQCsdyv-l4EQtvyE7PPHC3SHfYuJZPGg";
// 解析Header
JwsHeader header = Jwts
.parser()
.setSigningKey("symx.club")
.parseClaimsJws(token)
.getHeader();
System.out.println(header);
// 解析PayLoad
Claims claims = Jwts
.parser()
.setSigningKey("symx.club")
.parseClaimsJws(token)
.getBody();
System.out.println(claims);
// 解析Signature
String signature = Jwts
.parser()
.setSigningKey("symx.club")
.parseClaimsJws(token)
.getSignature();
System.out.println(signature);
}
}
6.解析结果
2、JWT 工具类
一般工作中都会将一些常用的方法整合成一个工具类方便使用
1.首先在配置文件中加入配置
jwt:
#JWT存储的请求头
requestHeader: Authorization
#JWT加解密使用的密钥
secret: symx.club
#JWT的有效时间(60*60*24*7)
expiration: 604800
#JWT负载中的开头
tokenStartWith: 'Bearer '
2.工具类
import club.gggd.demo.domain.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Description
* @Author srx
* @date 2023/11/30 22:42
*/
@Component
public class JwtUtil {
/**
* JWT存储的请求头
*/
@Value("${jwt.requestHeader}")
private String requestHeader;
/**
* 秘钥
*/
@Value("${jwt.secret}")
private String secret;
/**
* 有效时间
*/
@Value("${jwt.expire}")
private Integer expire;
/**
* jwt开头
*/
@Value("${jwt.tokenStartWith}")
private String tokenStartWith;
/**
* redis工具类
*/
@Autowired
private RedisUtils redisUtils;
/**
* 获取用户名
* @param token
* @return
*/
public String getUserNameByToken(String token) {
Claims claims = getClaimsByToken(token);
return claims != null ? claims.getSubject() : null;
}
/**
* 获取过期时间
* @param token
* @return Date
*/
public Date getExpiredByToken(String token) {
Claims claims = getClaimsByToken(token);
return claims != null ? claims.getExpiration() : null;
}
/**
* 获取Claims
* @param token
* @return
*/
private Claims getClaimsByToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
// 签名不一致异常
if (e instanceof SignatureException) {
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
// token过期异常
if (e instanceof ExpiredJwtException) {
throw new BusinessException(ResultCode.TOKEN_TIMEOUT);
}
// 如果都不是上面的则弹出token无效异常
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
return claims;
}
/**
* 计算过期时间
* @return
*/
private Date generateExpired() {
return new Date(System.currentTimeMillis() + expire * 1000);
}
/**
* 判断 Token 是否过期
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
Date expirationDate = getExpiredByToken(token);
return expirationDate.before(new Date());
}
/**
* 生成 Token
* @param user 用户信息
* @return
*/
public String generateToken(User user) {
String token = Jwts.builder()
.setSubject(user.getUserName())
.setExpiration(generateExpired())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
String key = "login:" + user.getUserName() + ":" + token;
redisUtils.set(key, token, expire / 1000);
return token;
}
/**
* 验证 Token
* @param token
* @return
*/
public Boolean validateToken(String token) {
final String username = getUserNameByToken(token);
String key = "login:" + username+ ":" + token;
Object data = redisUtils.get(key);
String redisToken = data == null ? null : data.toString();
return StringUtils.isNotEmpty(token) && !isTokenExpired(token) && token.equals(redisToken);
}
/**
* 移除 Token
* @param token
*/
public void removeToken(String token) {
final String username = getUserNameByToken(token);
String key = "login:" + username+ ":" + token;
redisUtils.del(key);
}
}
以上是 jwt 中一些比较常用的方法,大家可以根据自己的需求进行修改和添加
3、Spring Boot 整合 JWT
1.导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.配置文件
jwt:
#JWT存储的请求头
requestHeader: Authorization
#JWT加解密使用的密钥
secret: symx.club
#JWT的有效时间(60*60*24*7)
expire: 604800
#JWT负载中的开头
tokenStartWith: 'Bearer '
3.编写接口
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public WebResult login(HttpServletRequest request) {
// 这里直接模拟用户
User user = new User();
user.setUserName("admin");
String token = jwtUtil.generateToken(user);
return WebResult.ok(token);
}
@GetMapping("/test")
public WebResult test() {
return WebResult.ok("校验成功");
}
}
4.创建拦截器
@Component
public class JwtHandlerInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.requestHeader}")
private String requestHeader;
@Value("${jwt.tokenStartWith}")
private String tokenStartWith;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(requestHeader);
// 判断是否有token
if (StrUtil.isBlank(token)) {
throw new BusinessException(ResultCode.TOKEN_NOT_FOUND);
}
// 去掉token前缀
token = token.replace(tokenStartWith, "");
// 验证token是否存在redis,是否过期
Boolean valid = jwtUtil.validateToken(token);
if (!valid) {
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
return true;
}
}
5.注册拦截器
@Configuration
public class AuthWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private JwtHandlerInterceptor jwtHandlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 加入拦截器
registry.addInterceptor(jwtHandlerInterceptor)
// 所有请求都拦截
.addPathPatterns("/**")
// 除了登录接口
.excludePathPatterns("/auth/login");
}
}
6.验证
首先进行登录:
可以看到,能正常拿到 token,拿到 token 之后我们对需要 token 的接口在不放 token 的情况下请求一遍:
可以看到提示 token 为空,此时我们把 token 放进去:
可以正常访问,接下来我们在配置文件中把过期时间改小一点,比如 30 秒,等 30 秒之后再请求:
可以看到,提示 token 过期,最后我们自己生成一个 token 尝试呢?
这里我自己通过密钥生成了一个 token,并拿去请求
提示 token 无效
完结撒花 ❀❀❀