JWT入门

什么是JWT?

JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。
在数据传输过程中还可以完成数据加密、签名等相关处理。用于前端系统跟后端系统之间的信息传递。

可以把 JWT 想象成一张 “网络世界的身份证”。
它的全名是 JSON Web Token,意思是“基于 JSON 格式的网络令牌”。

当用户登录成功后,服务器会签发一张“身份证”(JWT)给前端,
前端以后访问服务器时,只要带上这张身份证,服务器就知道你是谁了,不用每次再登录。


JWT能做什么

授权

这是使用JWT最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用

信息交换

JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如:使用公钥/私钥对)

为什么用它?

传统方式:Session + Cookie

  • 用户登录成功后,服务器会在内存里保存一条记录(比如“用户A已登录”)
  • 然后给浏览器发一个小饼干(Cookie),里面写着一个“编号”(比如 session_id=12345)
  • 下次浏览器访问时,会自动带上这个 Cookie,服务器通过编号查出是谁,就知道你登录过
    这种方式的问题是:
  • 如果服务器重启或有多台服务器,session 不好同步
  • 状态都保存在服务器,占内存
  • 扩展性不太好
    于是,JWT 就出现了。

JWT的结构

在其紧凑的形式中,JWT由以点(.)分隔的三个部分组成,它们是:

  • Header
  • Payload
  • Signature

类似于xxxx.xxxx.xxxx格式,真实情况如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

并且你可以通过官网 https://jwt.io/#debugger-io 解析出三部分表示的信息

Header - 描述签名算法和类型

报头通常由两部分组成: Token的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256或 RSA)。
例如:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

最终这个 JSON 将由base64进行加密(该加密是可以对称解密的),用于构成 JWT 的第一部分

Payload - 存放用户信息

Token的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明: registered claims, public claims, and private claims。
例如:

1
2
3
4
5
{
"sub": "1234567890", // 注册声明
"name": "John Doe", // 公共声明
"admin": true // 私有声明
}

这部分的声明也会通过base64进行加密,最终形成JWT的第二部分

Signature - 确保内容没被篡改

要创建Signature,您必须获取编码的标头(header)、编码的有效载荷(payload)、secret、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256算法,签名将按以下方式创建:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +base64UrlEncode(payload),
secret
)

上面的JSON将会通过HMACSHA256算法结合secret进行加盐签名(私钥加密),其中header和payload将通过base64UrlEncode()方法进行base64加密然后通过字符串拼接 “.” 生成新字符串,最终生成JWT的第三部分

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,
secret就是用来进行jwt的签发和验证,
所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了

真实情况,一般是在请求头里加入Authorization,并加上Bearer标注最后是JWT(格式:Authorization: Bearer ):
example


JWT的Java实现

导入jjwt依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>

创建一个辅助类JwtUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.backend.util;

import io.jsonwebtoken.Jwts;

import javax.crypto.SecretKey;
import java.util.Date;

public class JwtUtil {

// 生成一个secret-key
private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build();

// 根据username生成token的方法
public static String generateToken(String username) {
long now = System.currentTimeMillis();
long expiry = 1000 * 60 * 5; // 5分钟有效期
return Jwts.builder()
.subject(username)
.issuedAt(new Date(now))
.expiration(new Date(now + expiry))
.signWith(SECRET_KEY)
.compact();
}

// 解密验证token的方法
public static String parseToken(String token) {
return Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload().getSubject();
}
}

将generateToken()插入到登录逻辑中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");

Optional<User> user = userService.login(username, password);

Map<String, Object> response = new HashMap<>();
if (user.isPresent()) {
String token = JwtUtil.generateToken(user.get().getUsername());

response.put("success", true);
response.put("message", "Login success");
response.put("username", user.get().getUsername());
response.put("token", token);
} else {
response.put("success", false);
response.put("message", "Invalid username or password");
}

return response;
}

将parseToken()插入到访问控制逻辑中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@GetMapping("/protected")
public Map<String, Object> protectedHello(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();

// 从请求头中获取 Authorization 字段
String authHeader = request.getHeader("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.put("success", false);
response.put("message", "Missing or invalid Authorization header");
return response;
}

// 提取 token(去掉 "Bearer " 前缀)
String token = authHeader.substring(7);

if (JwtUtil.parseToken(token) == null) {
response.put("success", false);
response.put("message", "Invalid or expired token");
} else {
response.put("success", true);
response.put("message", "login successfully!");
response.put("database", userService.userRepository.findAll());
}
return response;
}

前端处理(React+Vite+TS)

登录后保存token到浏览器会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const handleLogin = async (values: { username: string; password: string }) => {
try {
const res = await api.post("/auth/login", values);

// 后端返回格式:{ success: true, message: "...", username: {...} }
if (res.data?.success) {
messageApi.success("登录成功!");
// 等待0.75秒后跳转
setTimeout(() => {
navigate("/home");
}, 750);
localStorage.setItem("token", res.data.token); // 记录后端返回的jwt token
} else {
messageApi.error("登录失败,请检查用户名和密码。");
}
} catch (error: any) {
messageApi.error("Bad Connection to Server");
console.error("登录失败:", error);
}
}

访问资源时带上token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const visitData = async () => {
try {
const res = await api.get("/api/protected", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`, // 从本地存储中获取token
},
});
if (res.data?.success) {
messageApi.success(res.data?.message || "访问受保护资源成功!");
const users = res.data.database;
setUserList(users);
setAccessPermission(true);
} else {
messageApi.error(res.data?.message || "访问受保护资源失败,请重新登录。");
setAccessPermission(false);
}
} catch (error: any) {
messageApi.error("访问受保护资源失败,请重新登录。");
console.error("Error accessing protected resource:", error);
setAccessPermission(false);
}
}

然后基本就完成了!

参考文章:
JSON Web Token (JWT) Debugger
JWT最详细教程以及整合SpringBoot的使用(简洁易上手)
JWT详细讲解(保姆级教程)