5.JWT
官网地址:https://jwt.io/introduction/
JWT简称json web token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名(claim)等相关处理。
为什么使用JWT
- 登录授权:登录以后,后续请求只要携带jwt到服务器,服务器可以进行校验,从而允许路由,给予响应
- 信息交换:jwt中可以携带一些数据到达服务器,服务器可以进行获取
传统session弊端
- session是保存在服务器端的,对服务器是一种负担
- 跨服务器共享session需要我们解决
- 基于cookie识别用户,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
- 前后端分离在应用解耦后增加了部署的复杂性,且sessionId就是一个特征值,表达的信息不够丰富
基于JWT认证
JWT的结构
令牌组成
三个部分
- 标头(Header)
- 有效载荷(Payload)——会存放很多信息进去,密码不能放在这里
- 签名(Signature)——登录成功后下次请求需要通过签名来校验(验签)
Header
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子的。它并不是一种加密过程。
{
"alg" : "HS256",
"typ" : "JWT"
}
Payload
令牌的第二部分是有效负载,其中包含声明(claim)。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分。
{
"sub" : "123456789",
"name" : "John Doe",
"admin" : true
}
Signature
前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的header和payload以及我们提供的一个密钥secret,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过
HMACSHA256(base64UrlEncode(header)+”.”+base64UrlEncode(payload), secret);
加密技术
是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
- 对称加密,如AES
- 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
- 优势:算法公开、计算量小、加密速度快、加密效率高
- 缺陷:双方都使用同样密钥,安全性得不到保证
- 非对称加密,如RSA
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 不可逆加密,如MD5,SHA
- 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。
使用JWT
引入依赖
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
生成令牌
@SpringBootTest
public class TestJWT {
@Test
public void getToken() throws JOSEException {
// 生成JWT头部
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256) // 设置加密方式
.type(JOSEObjectType.JWT).build();// 设置jwt常量
System.out.println(header);
// 生成载荷部分
Payload payload = new Payload("admin");
System.out.println(payload);
// 生成签名器
JWSSigner jWSSigner = new MACSigner("token!Q2W#E$RWtoken!Q2W#E$RWtoken!Q2W#E$RW"); // 签名
// 生成签名
// 生成签名部分 base64URL(header) + base64URL(payload) + 加密算法(秘钥)
JWSObject jwsObject = new JWSObject(header, payload); // base64URL(header) + base64URL(payload)
jwsObject.sign(jWSSigner); // 签名部分
String token = jwsObject.serialize();
System.out.println(token);
}
}
解析令牌
@Test
public void verifyJwt() throws ParseException, JOSEException {
// 解析jwt
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2NvdW50IjoiYWRtaW4iLCJpZCI6IjEifQ.JBaeGE6TxjUIcvtMM7tH82-uDba4uqYfwMvMGE1ctgo";
JWSObject jwsObject = JWSObject.parse(jwt);
JWSVerifier jwsVerifier = null;
jwsVerifier = new MACVerifier(secretString);
// 验证JWT是否合法
boolean verify = jwsObject.verify(jwsVerifier);
System.out.println(verify);
// 获取JWT中的载荷信息
Payload payload = jwsObject.getPayload();
Map<String, Object> jsonObject = payload.toJSONObject();
jsonObject.forEach((k,v)->{
System.out.println(k + ":" + v);
});
}
封装JWT工具类
public class JwtUtil {
private static final String secretString = "token!Q2W#E$RWtoken!Q2W#E$RWtoken!Q2W#E$RW";
public static String getToken(Map map) throws JOSEException {
// 生成JWT头部
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256) // 设置加密方式
.type(JOSEObjectType.JWT).build();// 设置jwt常量
// 生成载荷部分
Payload payload = new Payload(map);
// MACSigner 对称加密
JWSSigner jWSSigner = new MACSigner(secretString);
// 生成签名
// 生成签名部分 base64URL(header) + base64URL(payload) + 加密算法(秘钥)
JWSObject jwsObject = new JWSObject(header, payload); // base64URL(header) + base64URL(payload)
jwsObject.sign(jWSSigner); // 签名部分
String token = jwsObject.serialize();
return token;
}
public static boolean verifyJwt(String jwt) throws ParseException, JOSEException {
// 解析jwt
JWSObject jwsObject = JWSObject.parse(jwt);
JWSVerifier jwsVerifier = new MACVerifier(secretString);
// 验证JWT是否合法
boolean verify = jwsObject.verify(jwsVerifier);
return verify;
}
public static Map getPayload(String jwt) throws ParseException {
// 获取JWT中的载荷信息
JWSObject jwsObject = JWSObject.parse(jwt);
Payload payload = jwsObject.getPayload();
Map<String, Object> jsonObject = payload.toJSONObject();
return jsonObject;
}
}
添加拦截器
登录成功之后所有后续请求需要携带有效token表示登录成功,才可以允许请求,每个请求都需要判断,可以利用拦截器拦截所有请求,但是排除 login 和查询全部书籍两个接口
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
// @Resource
// private JWTInterceptor jwtInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") // 添加要拦截的请求
.excludePathPatterns("/user/login", "/book"); // 排除某些请求不拦截
}
}
拦截器拦截请求之后,判断是否携带token,判断token是否合法
package com.woniu.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.woniu.utils.JWTUtil;
import com.woniu.utils.ResponseEnum;
import com.woniu.utils.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Classname JWTInterceptor
* @Description 拦截器的目的是拦截请求 判断用户是否登录 登录用户才可以放行 未登录 应该强行跳转去登录
* 1. token如何携带过来
* 2. 拿到token解析 是否合法
* @Date 2025/5/21 14:18
* @Created by pc
*/
// @Component
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// log.info("拦截器执行了");
// 验证token
response.setContentType("application/json;charset=utf-8");
// 这是从请求参数获取
// String token = request.getParameter("token");
// 从请求头中获取
String token = request.getHeader("token");
// 1.判断token是否为null
if(token == null){
log.warn("token为空,验证失败");
response.getWriter().write(JSONObject.toJSONString(ResponseUtil.get(ResponseEnum.USER_LOGIN_MISS)));
return false;
}
// 2.token存在 但是不合法
boolean flag ;
try {
flag = JWTUtil.verifyJwt(token);
if(!flag){
// 验证失败
log.error("token无效");
response.getWriter().write(JSONObject.toJSONString(ResponseUtil.get(ResponseEnum.USER_LOGIN_MISS)));
return false;
}
} catch (Exception e) {
log.error("token无效");
response.getWriter().write(JSONObject.toJSONString(ResponseUtil.get(ResponseEnum.USER_LOGIN_MISS)));
return false;
}
return true;
}
}
改造登录
登录成功之后要生成token
@Override
public ResponseUtil login(User user) throws JOSEException {
// 数据库根据用户名查询到的用户
User loginUser = userMapper.findByName(user.getAccount());
ResponseUtil r = null;
// 判断是否为空
if(loginUser == null){
r = ResponseUtil.get(ResponseEnum.USER_LOGIN_FAIL_1);
}else if(!loginUser.getPassword().equals(SecureUtil.md5(user.getPassword()+loginUser.getSalt()))){
// 比对密码的时候 用用户输入的密码+查询到这个用户的盐 再使用m5加密 比对查询到的密码
// 密码不匹配
r = ResponseUtil.get(ResponseEnum.USER_LOGIN_FAIL_2);
}else if(loginUser.getStatus() == 2){
// 账号为锁定状态
r = ResponseUtil.get(ResponseEnum.USER_LOGIN_FAIL_3);
}else{
// 登录成功 密码置空将用户信息返回给前端
loginUser.setPassword(null);
loginUser.setSalt(null);
// 生成jwt
// 设置载荷部分
Map<String,Object> payload = new HashMap<>();
// 用户id 用户名
payload.put("id",loginUser.getId());
payload.put("username",loginUser.getAccount());
String token = JWTUtil.getToken(payload);
// 封装相应给前端的数据
Map<String,Object> resultMap = new HashMap<>();
resultMap.put("token",token);
resultMap.put("userInfo",loginUser);
r = ResponseUtil.get(ResponseEnum.USER_LOGIN_SUCCESS,resultMap);
}
return r;
}
前端登录设置JWT
之前我们前端是将用户信息直接作为token,现在可以将jwt生成的token存入sessionStorage
后续所有请求需要携带token可以利用axios 封装的前端拦截器,从sessionStorage中获取token并放在请求头中随请求一起提交
// axios拦截器 可以在请求发出去的时候拦截下来
// server.interceptors.request.use(正常请求回调,错误回调) 这个回调函数有一个参数 我们一般用config
// return config 相当于放行请求 如果不放行没有响应数据
server.interceptors.request.use(config=>{
// console.log("请求拦截器执行了")
// 所有走 axios封装的请求 都会到这里执行下面代码
let token = sessionStorage.getItem("token");
// let token = 111;
config.headers["token"] = token
return config;
},error=>{
// console.log(error)
return Promise.reject(error)
})
拦截器导致跨域失效
如果我们使用的是跨域注解解决跨域,会出现跨域注解失效的问题,原因是拦截器会在请求到达控制器之前就被拦截,此时已经出现跨域问题,解决办法:可以利用过滤器优先于拦截器执行的机制,利用过滤器解决跨域问题
JWT失效前端处理
如果token被篡改,axios的响应拦截器中可以做对应的处理:token无效强制跳转登录页
server.interceptors.response.use(resp=>{
console.log("响应拦截器执行了")
// console.log(resp)
if(resp.data.code === 1005){
//token 无效
ElMessage.error(resp.data.msg)
sessionStorage.removeItem("token")
window.location.href = '/login'
}
return resp
},error=>{
return Promise.reject(error)
})
思维导图
本文来自博客园,作者:icui4cu,转载请注明原文链接:https://www.cnblogs.com/icui4cu/p/18892436