Spring Boot + JWT + jjwt 建立前后端分离登录认证(详细教程 + 工具类封装)入门教程

news/2025/11/9 22:50:44/文章来源:https://www.cnblogs.com/yangykaifa/p/19205482

1 Token

代替Session,就是一个加密的字符串

1.1 什么是token

Token是当用户第一次访问服务端,由服务端生成的一串加密字符串,以作后续客户端进行请求的一个通行令牌。当第一次登录后,服务器生成一个Token字符串,并将此字符串返回给客户端,以后客户端请求需要带上这个Token发送服务器,进行请求数据即可,无需再次带上用户名和密码。

1.2 使用场景

接口使用限制(聚合数据,天行数据,阿里云接口等);
登录场景(客户端登录后,服务器签发token返回客户端);
有时效的url链接控制(密保邮箱找回密码;邮箱激活账号);

2.jjwt组件

2.1 认识jjwt

生成解析token字符串的常用组件有auth0,jjwt,这里选用jjwt进行学习。

JJWT旨在成为最易于使用和理解的库,用于在jvm和Android上创建和验证JSON Web Token(JWT)。对JWT进行加密签名后,称为JWS

官网:https://github.com/jwtk/jjwt

JWT表示形式是一个字符串,该字符串包含三个部分,每个部分之间都用.进行分隔,每个部分都是Base64URL编码的。如下:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJpYXQiOjE3NjAyNTg0MDcsImV4cCI6MTc2MDI2MjAwN30.PtwFOZ9ndrnqt6IQO8DsxlSPt9G-wD11yBgHD6t-jNQ1

Header.Payload.Signature

第一部分:Header标头,说明算法、类型

{

“alg”: “HS256”

}

第二部分:Payload(body) 存放用户信息(Claims,声明),jwt中需要包含的Claims认证数据,claims分为标准cliams与自定义。

类型示例含义
标准字段(Registered Claims)sub, iss, iat, exp, aud 等JWT 规范里定义好的保留字段
自定义字段(Custom Claims)userId, role, email, nickname开发者自己添加的数据

{

“sub”: “24234242”

}

第三部分:Signature,签名,用来防篡改,它是通过将标头和正文的组合通过标头中指定的算法加密来计算的。起到鉴伪作用。

2.2 使用

  • 添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
完整的工具类

创建工具类,下面是工具类完整代码, 后面有拆解

@Component
public class JwtUtil {
private final Key key;
private final long expireTime;
// spring创建bean的时候,会先解析依赖, 扫描带有@Component, @ConfigurationProperties 的类,
// spring先创建了JWTConfig, 并把配置文件里面的属性读了进来
//在创建JwtUtil的时候发现需要JWTConfig, 就把已经准备好的JWTConfig传了进来
public JwtUtil(JWTConfig jwtConfig) {
// Base64 解码后生成 Key
this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtConfig.getSecretKey()));
this.expireTime = jwtConfig.getExpireTime();
}
/**
* 生成密钥 每次执行这段代码都会生成一个不同的密钥,所以它只在开发阶段执行一次,
* 拿到密钥字符串后,就可以直接写进配置文件:
*/
public static void generateKey() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println("生成的密钥是:" + secretString);
}
/**
* 生成token
* @return
*/
public String generateToken(String userId) {
Instant now = Instant.now();   //当前时间
return Jwts.builder()
// 设置payload标准字段
.setSubject(userId) // 表示哪个用户的 Token
.setIssuedAt(Date.from(now))  //签发时间
.setExpiration(Date.from(now.plusMillis(this.expireTime))) //过期时间
//设置自定义的payload
// .claim("role", "user")   // 需要就添加自定义字段
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析并验证 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)     // 设置签名的 key
.build()
.parseClaimsJws(token)  // 会自动验证签名、过期时间,如果签名错误或过期,会直接抛异常
.getBody();             // 返回 payload 部分 (Claims)
} catch (ExpiredJwtException e) {
throw new RuntimeException("token expired");
} catch (JwtException e) {
throw new RuntimeException("token invalid");
}
}
}
拆解代码
  • 生成密钥
//JWT规范确定了12种标准签名算法:3种对称密钥算法和9种非对称密钥算法。
//HS256:使用SHA-256的HMAC
//HS384:使用SHA-384的HMAC
//HS512:使用SHA-512的HMAC
//ES256:使用P-256和SHA-256的ECDSA
//ES384:使用P-384和SHA-384的ECDSA
//ES512:使用P-521和SHA-512的ECDSA
//RS256:使用SHA-256的RSASSA-PKCS-v1_5
//RS384:使用SHA-384的RSASSA-PKCS-v1_5
//RS512:使用SHA-512的RSASSA-PKCS-v1_5
//PS256:使用SHA-256和MGF1与SHA-256的RSASSA-PSS
//PS384:使用SHA-384和MGF1与SHA-384的RSASSA-PSS
//PS512:使用SHA-512和MGF1与SHA-512的RSASSA-PSS
//前三者是对称秘钥算法,后9个是非对称算法。
//以下代码采用HS256对称秘钥算法。
/**
*   生成密钥 每次执行这段代码都会生成一个不同的密钥,所以它只在开发阶段执行一次,
*   拿到密钥字符串后,就可以直接写进配置文件:
*/
public static void generateKey() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println("生成的密钥是:"+secretString);
}

image-20251012094725792

  • 复制生成的密钥写入配置文件 application.yml中

    jwt:
    secret-key: B8GMT15rEkshJBGlGu3W7ClQGg9yavvU/VHjTOUOpVg=
    expire-time: 3600000  # 1小时(可选)
  • 创建配置属性类

    /**
    *  配置属性类
    */
    @Data
    @Component
    @ConfigurationProperties(prefix = "jwt")
    public class JWTConfig {
    private String secretKey;
    private Long expireTime;
    }

    Spring Boot 会在加载配置文件时自动识别多种常见命名风格,并互相兼容。

    比如下面这些写法在绑定时被认为是等价的

YAML 中的写法Java 属性对应
secret-keysecretKey
secret_keysecretKey
SECRET_KEYsecretKey
SecretKeysecretKey
secretKeysecretKey

Spring Boot 允许你在配置文件中自由使用中划线、小写、下划线或驼峰格式。最终都会被正确映射到 Java Bean 属性中。

  • 生成token
/**
* 生成token
* @return
*/
public String generateToken(String userId) {
Instant now = Instant.now();   //当前时间
return Jwts.builder()
// 设置payload标准字段
.setSubject(userId) // 表示哪个用户的 Token
.setIssuedAt(Date.from(now))  //签发时间
.setExpiration(Date.from(now.plusMillis(this.expireTime))) //过期时间
// .claim("role", "user")   // 需要就添加自定义字段
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
  • 解析token
/**
* 解析并验证 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)     // 设置签名的 key
.build()
.parseClaimsJws(token)  // 会自动验证签名、过期时间,如果签名错误或过期,会直接抛异常
.getBody();             // 返回 payload 部分 
} catch (ExpiredJwtException e) {
throw new RuntimeException("token expired");
} catch (JwtException e) {
throw new RuntimeException("token invalid");
}
}

3.登录场景使用token

后端部分代码

//写在controller里面
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + token); //这里存储的数据在header里面
httpHeaders.add("Access-Control-Expose-Headers", "Authorization");   //这里要暴露出去
Result<Object> result = Result.success("这里的数据返回到body里面");ResponseEntity<Result<Object>> responseEntity = new ResponseEntity<>(result, httpHeaders, HttpStatus.OK);
注意

若要让前端能正确读取到响应头中的自定义字段(如 Authorization),

后端需显式暴露该字段,例如在 Spring Boot 中:

image-20251013110856343

image-20251013110912784

@Configuration
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
public WebConfig(JwtInterceptor jwtInterceptor) {
this.jwtInterceptor = jwtInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/login",
"/error",
"/public/**",
"/static/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/**"
)
.order(Ordered.HIGHEST_PRECEDENCE);
}
@Override
public void addCorsMappings(CorsRegistry r) {
r.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("Authorization","Content-Type","X-Requested-With")
.exposedHeaders("Authorization","Content-Disposition")
// 和 Access-Control-Expose-Headers: Authorization 是同一个目的
.allowCredentials(true)
.maxAge(3600);
}
}
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
//    @Autowired
//    private RedisTemplate<String, String> redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {//放行预检请求if("OPTIONS".equalsIgnoreCase(request.getMethod())) {return true;}String header = request.getHeader("Authorization");if (header == null || !header.startsWith("Bearer ")) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}String token = header.substring(7);try {Claims claims = jwtUtil.parseToken(token);String userId = claims.getSubject();////            // 可选:在 Redis 中校验 token//            String redisToken = redisTemplate.opsForValue().get("login:token:" + userId);//            if (redisToken == null || !redisToken.equals(token)) {//                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);//                return false;//            }// 保存到请求作用域,Controller 可以直接拿 userIdrequest.setAttribute("userId", userId);return true;} catch (JwtException e) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}}}
  • 登录接口省略了, 就是判断有没有token, 用户登录成功就下发token, 后续访问接口只需要验证是否有token

    对此接口进行测试

image-20251012171701755

前端部分代码

  • 前端接收token, 并存储起来, 下次发请求携带即可. 用请求过滤器和pinia进行实现,这里不展示
console.log(res.headers.authorization)

image-20251013102341452

  • token 在响应头(Authorization)中返回;

  • 前端可通过 res.headers.authorization 获取;

  • 存入 Pinia 或 localStorage 后,在 Axios 请求拦截器中自动携带;

  • 这样后续接口请求即可实现免登录访问。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python:pip配置国内源

一,创建目录 说明:是在用户的home目录下创建 liuhongdi@liuhongdi-pc:~$ mkdir .pip liuhongdi@liuhongdi-pc:~$ cd .pip liuhongdi@liuhongdi-pc:~/.pip$ vi pip.conf 二,在配置文件中添加国内源 pip.conf的内容: …

nest目录结构

NestJS目录结构 1. NestJS目录结构 user: nestjs如何组织目录结构assistant: 当然!在 NestJS 中,组织良好的目录结构对于项目的可维护性、可扩展性和团队协作至关重要。虽然没有唯一的“正确”答案,但社区已经形成了…

第十一届中国大学生程序设计竞赛 女生专场(CCPC 2025 Womens Division)题解

目录Problem A. 环状线Problem B. 爬山Problem C. 短视频Problem D. 网络改造Problem E. 购物计划Problem F. 丝之歌Problem G. 最大公约数Problem H. 缺陷解码器Problem I. 调色滤镜Problem J. 后鼻嘤Problem K. 左儿…

什么?从分子变化到四大关键特征解析就是重排反应

什么?从分子变化到四大关键特征解析就是重排反应2025-11-09 22:41 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; displa…

高三日记

开坑于 \(11.9\)。 一模考完终于有时间写一下了,先补一下历次考试战绩吧。 G12 名校协作体(\(9.1 \sim 9.2\)) 退役 \(1.5\) 个月,目标是上一下特控线。 语文考完直接破防了。 数学屁也不会。 英语听力错三个,根本…

AI agent framework langgraph

https://docs.langchain.com/oss/python/langgraph/workflows-agentsgraph vs workflow Agents are typically implemented as an LLM performing actions using tools. They operate in continuous feedback loops, a…

计算机毕设项目推荐:基于SpringBoot+Vue的非物质文化遗产再创新系统 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

基于实际字节码解析Python链式赋值:从ls1[i]=2到a=b=c=10的完整机制

基于实际字节码解析Python链式赋值:从ls1[i]=2到a=b=c=10的完整机制 针对你提出的“无固定‘左右顺序’?”的疑问,结合你提供的真实字节码(dis模块输出),我们可以明确:Python链式赋值不存在绝对统一的“左→右”…

实用指南:基于python写的PDF表格提取到excel文档

实用指南:基于python写的PDF表格提取到excel文档pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas",…

侯捷C++面向对象高级开发(上)

一、complex类 1、内联函数 class complex { public:complex(double r=0,double i=0):re(r),im(i){}complex& operator += (const complex&);double real () const {return re;}double imag () const {return …

企业微信scrm源码开发-渠道活码数据库表设计

wx: llike620CREATE TABLE `wxwork_channel` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(100) NOT NULL DEFAULT COMMENT 活码名称,`config_id` varchar(64) NOT NULL DEFAULT COMMENT 企微返回的配…

Python助力数据分析如何用Pandas高效处理大规模资料

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

SDD驱动开发

基于 SDD 驱动的开发方法实践测试 记录基于 AI 设计与开发工程,实践总结一套方法 程序员使用 AI 开发 Top 5 常见问题需求描述不清导致 AI 理解偏差 程序员在给 AI 描述需求时,常常因为表达不准确或缺乏上下文,导致…

Redis 缓存 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

动态规划:使用最小花费爬楼梯

题目力扣链接 代码随想录链接dp数组定义:到达此台阶的最小体力为dp[i]递推公式:前两个台阶最小体力值加这两个台阶的cost。dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])dp初始化:dp[0] = 0,dp[…

OddAgent:轻松手搓一个你自己的“小艺”、“小爱同学”

想自己动手来手搓一个完全属于你自己的“小爱同学”、“小艺”吗?如果有你这么一个想法,而又不知道该如何开始的话,那么OddAgent项目可以成为你非常容易上手的开源项目。想自己动手来手搓一个完全属于你自己的“小爱…

使用UnsafeAccessor 访问私有字段

UnsafeAccessor 允许在 不依赖反射 的情况下,高效地访问私有字段、属性、方法甚至构造函数。它的使用场景非常明确:你需要访问一个类型的私有成员,但你不能或不想改变该类型的可见性设计。支持AOT。UnsafeAccessor …

[PTA]龟兔赛跑

题目描述题源:龟兔赛跑 - PTA 题意:乌龟与兔子在同一起点、同一时刻沿环形跑道赛跑。乌龟以 \(3\text{ m/s}\) 匀速前进;兔子以 \(9\text{ m/s}\) 奔跑,但每隔 \(10\) 分钟回头观察一次:若此时已领先乌龟,则停下…

数组参数的函数传递

数组参数的函数传递package org.example;public class Main {public static void main(String[] args){Main s=new Main();s.test(1,2);s.test();}public void test(int...i)//可变传参必须放最后,可用作数组传参{if(…

【狂神说Java】Mybatis最新完整教程IDEA版通俗易懂 P1什么是Mybatis P2第一个Mybatis程序

1、简介 环境说明: jdk 8 + MySQL 5.7.19 maven-3.6.1 IDEA 学习前需要掌握: JDBC MySQL Java 基础 Maven Junit 1.1、什么是MyBatis MyBatis 是一款优秀的持久层框架 MyBatis 避免了几乎所有的 JDBC 代码…