SpringBoot 项目如何集成 JWT

SpringBoot 项目如何集成 JWT

JWT

JSON Web Token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。

在 Oauth2 中,其实就是返回访问令牌 (access_token)代替用户名和密码来使用,访问令牌只需要满足唯一性,不可猜测性等条件即可,那么 JWT 结构化令牌也可以达到这样的效果,且内部可以包含一些更有效的信息

{
"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNzMzNTYwMzY5fQ.AC3oOXbpz2cLYnCHTcYUCQxdTe_dqvwSSDpGi7W2IXg" 
}

比如上面这个 token ,看起来只是一串由三个.分割出的字符串罢了,但是如果你放到 https://jwt.io/ 上进行解析,就可以得到下面的结果:

image-20241207143706041

JWT 可以分为以下三个部分:

  • HEADER 表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法。
  • PAYLOAD 表示是 JWT 的数据体,代表了一组数据。
  • SIGNATURE 表示对 JWT 信息的签名,虽然有了 HEADER 和 PAYLOAD,信息已经可以进行传输了,但是我们为了安全,需要对其进行加密签名,SIGNATURE 就是对其进行加密签名的结果。

为什么需要加密签名呢,以登录流程为例,后端服务在用户登录成功后,返回了 jwt 并存放到了 cookie 里,前端每次发请求都携带这个 jwt,后端收到后,根据加密时候所用的加密串进行解密,如果解密成功,说明 jwt 没有被伪造,继续执行请求,否则认为这个 jwt 是伪造的,返回错误信息,需要前端重新登录,获取新 jwt。

SpringBoot 集成 JWT

  1. 登录时的返回信息中为用户新增token字段
package com.zwj.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
// 这个类用于登录成功时返回,由前端存入 cookie 里,用于一些展示,比如 #{username},欢迎使用
public class UserCookie {
    private String username;
    private String email;
    private String token;
}
package com.zwj.utils;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.zwj.pojo.Admin;
import com.zwj.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

@Component
@Slf4j
public class TokenUtil {
    // 生成 Token 的方法
    public static String genToken(String adminId, String sign) {
        return JWT.create().withAudience(adminId) // 将 adminId 保存到 token 里面,作为负载
                .withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
                .sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的签名密钥
    }
}
package com.zwj.controller;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.MD5;
import com.zwj.controller.request.AdminPageRequest;
import com.zwj.controller.request.UserPageRequest;
import com.zwj.pojo.Admin;
import com.zwj.service.AdminService;
import com.zwj.utils.MD5Util;
import com.zwj.utils.Result;
import com.zwj.utils.TokenUtil;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.security.Security;
import java.util.Date;
import java.util.List;

@RestController
@RequestMapping("/admin")
public class AdminController {

    @Resource
    AdminServiceImpl adminService;

    @PostMapping("/login")
    public Result<?> login(@RequestBody Admin user){
        // 密码进行 MD5 加密
        user.setPassword(MD5Util.MD5Encode(user.getPassword(),"UTF-8"));
        // 找到登录的这个用户
        Admin admin = adminService.getAdminByUsernameAndPassword(user.getUsername(),user.getPassword());
        if (admin != null){
            if (admin.getStatus() == 1){
                UserCookie cookie = new UserCookie();
                // 为其生成 jwt,负载是用户ID,签名密钥是用户密码的MD5加密结果
                String token = TokenUtil.genToken(String.valueOf(admin.getId()), admin.getPassword());
                cookie.setToken(token);
                cookie.setUsername(admin.getUsername());
                // 返回这个用户信息,用于前端存入 cookie
                return Result.success(cookie);
            } else {
                return Result.error("您的权限已冻结!");
            }
        }
        return Result.error("用户名或密码错误!");
    }

}
  1. 前端将信息存入 cookie 中
request.post("/admin/login", this.admin).then(res => {
    if (res.code === '200'){
        if (res.data != null){
            // 登录成功,将返回结果里的数据存入 cookie 中,key 是 'user'
            this.loginAdmin = res.data;
            Cookies.set('user', JSON.stringify(this.loginAdmin));
        }
    } else {
        this.$notify.error(res.msg);
    }
})
  1. 前端设置后续每次发送请求时,带上这个 token
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    // 从 cookie 里取当前登录者信息
    let user = Cookies.get('user');
    if (user != null){
        // 如果能取到,放在 header 里
        config.headers['token'] = JSON.parse(user).token;
    }
    return config;
}, error => {
    return Promise.reject(error)
});
  1. 后端设置拦截器,拦截除登录、重置密码等不需要 token 外的所有请求
package com.zwj.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements  WebMvcConfigurer {

    @Autowired
    JwtInterceptor jwtInterceptor;

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // 指定controller统一的接口前缀
        configurer.addPathPrefix("/api", clazz -> clazz.isAnnotationPresent(RestController.class));
    }

    // 加自定义拦截器JwtInterceptor,设置拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里的 jwtInterceptor 是我们自定义的,定义内容在下一个代码块
        registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**")
            .excludePathPatterns("/api/admin/login","/api/files/**");
    }
}
package com.zwj.config;

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zwj.pojo.Admin;
import com.zwj.excepion.ServiceException;
import com.zwj.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    private static final String ERROR_CODE_401 = "401";

    @Resource
    private AdminService adminService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 从 header 里取 token
        String token = request.getHeader("token");

        // 如果没有直接抛出异常
        if (StrUtil.isBlank(token)) {
            throw new ServiceException(ERROR_CODE_401, "无token,请重新登录");
        }
        // 获取 token 中的 adminId
        String adminId;
        Admin admin;
        try {
            adminId = JWT.decode(token).getAudience().get(0);
            // 根据 token 中的 adminId 查询数据库
            admin = adminService.getAdminById(Integer.parseInt(adminId));
        } catch (Exception e) {
            String errMsg = "token验证失败,请重新登录";
            log.error(errMsg + ", token=" + token, e);
            throw new ServiceException(ERROR_CODE_401, errMsg);
        }
        if (admin == null) {
            throw new ServiceException(ERROR_CODE_401, "用户不存在,请重新登录");
        }

        try {
            // 用户密码加签验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(admin.getPassword())).build();
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException(ERROR_CODE_401, "token验证失败,请重新登录");
        }
        return true;
    }
}

以上就是 SpringBoot 集成 JWT 的流程,其中的重点:

  1. 生成 token 时的内容和顺序

image-20241207150814346

比如这里,设置了 audience 的值是 adminId,签名密钥是该用户的密码的 MD5 加密结果

  1. 验证的时候要按照相同的顺序取,使用相同的密钥验证

image-20241207151029025

当然也可以不选择密码作为密钥验证,使用任意字符串也可以,但是使用密码做验证有这样的好处:如果用户此时修改了密码,那么这个 JWT 也就随之失效了,验证签名的时候会校验失败。但是如果使用其他常量作为密钥,就只能等待 JWT 主动到期,在修改密码到主动到期这段时间,JWT 依然可以被正常使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/932044.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【innodb阅读笔记】之 Innodb行记录格式 (Compact 行格式)

一、Innodb行记录格式 innodb 存储引擎同大多数数据库一样&#xff0c;记录是以行的形式存储的。这意味着页中保存的一行行的数据。在 mysql 5.7 版本中&#xff0c;默认格式为 Dynamic&#xff0c;可以通过命令查看当前表的行格式&#xff0c;其中 row_format 表示当前表行记录…

交易所 Level-2 历史行情数据自动化导入攻略

用户部署完 DolphinDB 后&#xff0c;需要将历史股票数据批量导入数据库&#xff0c;再进行数据查询、计算和分析等操作。DolphinDB 开发了 ExchData 模块&#xff0c;主要用于沪深交易所 Level-2 行情原始数据的自动化导入&#xff0c;目前已支持的数据源包括&#xff1a; 沪…

加载内核映像文件

将kernel转换成elf文件格式&#xff0c;不能直接从loader直接跳转到0x100000&#xff0c;需要解析&#xff0c;提取出代码和数据出来&#xff0c;放到0x10000&#xff08;64kb&#xff09;的位置&#xff0c;1M的位置只是存放elf文件的位置。 4.10加载内核映像文件2 common/el…

【数字电路与逻辑设计】实验一 序列检测器

文章总览&#xff1a;YuanDaiMa2048博客文章总览 【数字电路与逻辑设计】实验一 序列检测器 一、实验内容二、设计过程&#xff08;一&#xff09;作出状态图或状态表&#xff08;二&#xff09;状态化简&#xff08;三&#xff09;状态编码 三、源代码&#xff08;一&#xff…

怎么实现邮件营销自动化?

邮件营销能够出色地帮助我们与客户建立良好关系。无论是新客户还是老客户&#xff0c;都可以通过邮件来达成较为良好的客户关系。然而&#xff0c;从消费者的角度来看&#xff0c;每个人都有自己独特的习惯和特点&#xff0c;没有人希望收到千篇一律、营销意味过重的邮件。因此…

【LeetCode: 203. 移除链表元素 + 链表】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【opencv入门教程】12. 矩阵初始化

文章选自&#xff1a; 一、 数据类型 建立矩阵必须要指定矩阵存储的数据类型&#xff0c;图像处理中常用的几种数据类型如下&#xff1a;包括数据位深度8位、32位&#xff0c;数据类型U:uchar、F:float型以及通道数C1&#xff1a;单通道、C3&#xff1a;三通道、C4&#xff…

Jupyter Notebook认识、安装和启动以及使用

Jupyter Notebook认识、安装和启动以及使用 Jupyter Notebook认识、安装和启动以及使用 Jupyter Notebook认识、安装和启动以及使用一、认识Jupyter Notebook1.1 Jupyter Notebook概述1.2 Jupyter Notebook 重要特性(1)交互式代码执行(2)支持多种编程语言(3)富文本编辑(4)代码高…

SQL语句中AND与OR操作符的优先级问题

在SQL中&#xff0c;当AND和OR操作符同时出现时&#xff0c;优先级的处理可能会导致查询结果与预期不符。为了说明这一问题&#xff0c;我们可以看一个实际的例子。 假设需要查询价格在10美元及以上&#xff0c;且由DLL01或BRS01制造的所有产品。可以使用如下SQL语句&#xff…

FreeRTOS实现UART通信

串口通信 速战速决形式&#xff0c;大家走一遍就通&#xff1b; 本次实验验证&#xff1a; 配置文件 4、打开CubeMX 5、选择芯片型号&#xff0c;然后点击开始项目 6、配置时钟 配置烧录引脚&#xff0c;与FreeRTOS系统时钟 选择FreeRTOS 这里已经默认有一个任务&…

FaRM译文

No compromises: distributed transactions with consistency, availability, and performance Aleksandar Dragojevic, Dushyanth Narayanan, Edmund B. Nightingale, Matthew Renzelmann, Alex Shamis, Anirudh Badam, Miguel Castro Microsoft Research 目录 摘要 1. 引…

Ubuntu22.04深度学习环境安装【Anaconda+Pycharm】

anaconda可以提供多个独立的虚拟环境&#xff0c;方便我们学习深度学习&#xff08;比如复现论文&#xff09;&#xff1b; Pycharm编辑器可以高效的编写python代码&#xff0c;也是一个很不错的工具。 下面就记录下Ubuntu22.04的安装流程&#xff1a; 1.Anaconda安装 下载Ana…

Angular由一个bug说起之十一:排序之后无法展开 Row

问题现象 在使用 Material Table 时&#xff0c;排序功能触发了一个奇怪的 Bug&#xff1a;表格的 Row 无法展开。最终排查发现&#xff0c;问题的根源在于 trackBy 的错误使用。trackBy 方法接受两个参数&#xff1a;index&#xff08;数据索引&#xff09;和 row&#xff08;…

【无标题】建议用坚果云直接同步zotero,其他方法已经过时,容易出现bug

created: 2024-12-06T16:07:45 (UTC 08:00) tags: [] source: https://zotero-chinese.com/user-guide/sync author: 数据与文件的同步 | Zotero 中文社区 Excerpt Zotero 中文社区&#xff0c;Zotero 中文维护小组&#xff0c;Zotero 插件&#xff0c;Zotero 中文 CSL 样式 数…

【React】React常用开发工具

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、React DevTools二、Redux DevTools三、Create React App 前言 React 是一种用于构建用户界面的流行 JavaScript 库&#xff0c;由于其灵活性、性能和可重用…

Thonny IDE + MicroPython + ESP32 + GY-302 测量环境中的光照强度

GY-302是一款基于BH1750FVI光照强度传感器芯片的模块。该模块能够直接测量出环境中的光照强度&#xff0c;并将光照强度转换为数字信号输出。其具体参数如下表所示。 参数名称 参数特性 测量范围 0-65535 LX 测量精度 在环境光下误差小于20%&#xff0c;能够自动忽略50/60…

华为HCIP AI EI Developer总结和备考建议

华为HCIP AI EI Developer总结和备考建议 最近空余时间考了一个华为的HCIP认证&#xff0c;属于AI方向的四个其中一个&#xff0c;这个主要侧重于机器学习和深度学习的基础知识&#xff0c;比较偏理论。 一、备考时间 根据华为官方建议&#xff0c;培训时长是9天&#xff0c;…

Jenkins 的HTTP Request 插件为什么不能配置Basic认证了

本篇遇到的问题 还是因为Jenkins需要及其所在的OS需要升级&#xff0c;升级策略是在一台新服务器上安装和配置最新版本的Jenkins&#xff0c; 当前的最新版本是&#xff1a; 2.479.2 LTS。 如果需要这个版本的话可以在官方站点下载&#xff0c;也可以到如下地址下载&#xff1…

运费微服务和redis存热点数据

目录 运费模板微服务 接收前端发送的模板实体类 插入数据时使用的entity类对象 BaseEntity类 查询运费模板服务 新增和修改运费模块 整体流程 代码实现 运费计算 整体流程 总的代码 查找运费模板方法 计算重量方法 Redis存入热点数据 1.从nacos导入共享redis配置…