5.JWT

官网地址:https://jwt.io/introduction/

JWT简称json web token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名(claim)等相关处理。

为什么使用JWT

  • 登录授权:登录以后,后续请求只要携带jwt到服务器,服务器可以进行校验,从而允许路由,给予响应
  • 信息交换:jwt中可以携带一些数据到达服务器,服务器可以进行获取

传统session弊端

  • session是保存在服务器端的,对服务器是一种负担
  • 跨服务器共享session需要我们解决
  • 基于cookie识别用户,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
  • 前后端分离在应用解耦后增加了部署的复杂性,且sessionId就是一个特征值,表达的信息不够丰富

基于JWT认证

image

JWT的结构

令牌组成

三个部分

  • 标头(Header)
  • 有效载荷(Payload)——会存放很多信息进去,密码不能放在这里
  • 签名(Signature)——登录成功后下次请求需要通过签名来校验(验签)

标头通常由两部分组成:令牌的类型(即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);

image

加密技术

是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如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)
})

思维导图

image

posted @ 2025-05-23 09:39  icui4cu  阅读(20)  评论(0)    收藏  举报