← 返回首页
文章
2026-03-31
JWT 完全指南
JWT 完全指南 零基础也能看懂的 JWT 教程,从概念到实战,Java 和 Node.js 双语言实现,覆盖登录认证、API 授权等真实场景 目录 1. 核心概念 2. JWT 结构详解 3. JWT 工作流程 4....
JWT 完全指南 #
零基础也能看懂的 JWT 教程,从概念到实战,Java 和 Node.js 双语言实现,覆盖登录认证、API 授权等真实场景
目录 #
一、核心概念 #
1.1 什么是 JWT?(大白话) #
JWT(JSON Web Token) 是一种用于在网络上安全传输信息的"通行证"。
打个比方:
- 传统 Session = 你的身份证存在派出所,每次办事都要派出所确认
- JWT = 身份证发给你自己保管,上面有防伪标记,办事时直接出示
传统 Session 流程:
用户登录 → 服务器创建 Session → 返回 Session ID
用户请求 → 携带 Session ID → 服务器查找 Session → 验证身份
JWT 流程:
用户登录 → 服务器生成 JWT → 返回给用户
用户请求 → 携带 JWT → 服务器验证签名 → 直接信任(无需存储)1.2 为什么需要 JWT? #
传统 Session 的问题:
| 问题 | 说明 |
|---|---|
| 服务器压力 | 每个用户都要存 Session,用户多了内存爆炸 |
| 扩展困难 | 多台服务器要共享 Session,需要额外存储(Redis) |
| 跨域问题 | 前后端分离、移动端、小程序,Cookie 不好用 |
| 移动端不友好 | 原生 App 不支持 Cookie |
JWT 的优势:
| 优势 | 说明 |
|---|---|
| 无状态 | 服务器不需要存储,JWT 自带信息 |
| 跨平台 | Web、App、小程序都可以用 |
| 易扩展 | 水平扩展不需要考虑 Session 同步 |
| 跨域友好 | 放在 Header 里,不依赖 Cookie |
1.3 JWT 的典型应用场景 #
| 场景 | 说明 |
|---|---|
| 用户认证 | 登录后发放 Token,后续请求携带验证 |
| 信息交换 | 安全地在各方之间传输信息(有签名防篡改) |
| 单点登录(SSO) | 一次登录,多个系统共享认证 |
| API 授权 | 第三方应用访问 API |
| 无状态服务 | 微服务架构中传递用户信息 |
1.4 核心名词解释 #
| 名词 | 大白话解释 |
|---|---|
| Token | 令牌,一串加密字符串,用于证明身份 |
| Payload | 载荷,JWT 中存储的实际数据(用户 ID、过期时间等) |
| Signature | 签名,用密钥生成的防伪标记,验证 JWT 没被篡改 |
| Secret | 密钥,服务器保管,用于签名和验证 |
| Claim | 声明,Payload 中的每个字段叫一个 Claim |
| Expiration | 过期时间,JWT 的有效期 |
| Refresh Token | 刷新令牌,用于获取新的 Access Token |
二、JWT 结构详解 #
2.1 JWT 的三部分结构 #
JWT 由三部分组成,用 . 分隔:
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c2.2 Header(头部) #
Header 描述 JWT 的元信息:
{
"alg": "HS256", // 签名算法:HS256、RS256、ES256 等
"typ": "JWT" // 令牌类型:固定为 JWT
}Base64 编码后:
const header = { alg: "HS256", typ: "JWT" };
const encoded = btoa(JSON.stringify(header));
// 结果:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9常用签名算法:
| 算法 | 类型 | 特点 |
|---|---|---|
| HS256 | 对称加密 | 用同一个密钥签名和验证,速度快 |
| RS256 | 非对称加密 | 私钥签名,公钥验证,更安全 |
| ES256 | 非对称加密 | 椭圆曲线,密钥更短,性能更好 |
2.3 Payload(载荷) #
Payload 存储Claim(声明):
{
// 标准声明(Registered Claims)
"iss": "myapp.com", // 签发者
"sub": "1234567890", // 主题(通常是用户 ID)
"aud": "myapp.com", // 接收者
"exp": 1516239022, // 过期时间
"nbf": 1516239022, // 生效时间
"iat": 1516239022, // 签发时间
"jti": "unique-id", // JWT ID(唯一标识)
// 自定义声明
"name": "John Doe",
"role": "admin",
"email": "john@example.com"
}标准声明说明:
| 声明 | 全称 | 说明 |
|---|---|---|
| iss | Issuer | 签发 JWT 的主体 |
| sub | Subject | JWT 的主题(用户 ID) |
| aud | Audience | JWT 的接收者 |
| exp | Expiration | 过期时间(时间戳) |
| nbf | Not Before | 生效时间(在此之前无效) |
| iat | Issued At | 签发时间 |
| jti | JWT ID | JWT 的唯一标识(用于防止重放攻击) |
⚠️ 注意:Payload 不是加密的,只是 Base64 编码,不要存敏感信息!
// ❌ 错误:不要在 Payload 中存敏感信息
{
"password": "123456", // 任何人都能看到!
"creditCard": "xxxx-xxxx" // 危险!
}
// ✅ 正确:只存非敏感的用户标识
{
"userId": "12345",
"role": "admin"
}2.4 Signature(签名) #
签名用于验证 JWT 没有被篡改:
Signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)签名过程:
// 伪代码
const headerEncoded = base64UrlEncode(header);
const payloadEncoded = base64UrlEncode(payload);
const unsignedToken = headerEncoded + "." + payloadEncoded;
// 用密钥签名
const signature = HMACSHA256(unsignedToken, secret);
// 最终 JWT
const jwt = unsignedToken + "." + base64UrlEncode(signature);为什么签名能防篡改?
攻击者想修改 Payload(比如把 role 从 user 改成 admin):
1. 修改 Payload:{ "role": "admin" }
2. Base64 编码新 Payload
3. 重新组合 JWT
问题:攻击者没有密钥,无法生成正确的签名!
服务器验证签名失败 → 拒绝请求2.5 Base64Url 编码 #
JWT 使用 Base64Url 编码(不是普通 Base64):
// Base64: + / =
// Base64Url: - _ (去掉填充)
// 普通Base64
"abc+def/ghi="
// Base64Url(JWT 用这个)
"abc-def_ghi"
// 转换函数
function base64UrlEncode(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return atob(str);
}三、JWT 工作流程 #
3.1 登录认证流程 #
┌─────────────────────────────────────────────────────────────┐
│ JWT 登录认证流程 │
└─────────────────────────────────────────────────────────────┘
用户 服务器
│ │
│ ① 登录请求 │
│ POST /login │
│ {username, password} │
│────────────────────────>│
│ │ ② 验证用户名密码
│ │ 查询数据库
│ │
│ │ ③ 生成 JWT
│ │ 签名 + 设置过期时间
│ │
│ ④ 返回 JWT │
│<────────────────────────│
│ {token: "xxx.yyy.zzz"}│
│ │
│ ⑤ 存储 JWT │
│ localStorage │
│ │
│ ⑥ 业务请求 │
│ GET /api/user │
│ Header: Authorization: Bearer xxx.yyy.zzz
│────────────────────────>│
│ │ ⑦ 验证 JWT
│ │ - 解析 JWT
│ │ - 验证签名
│ │ - 检查过期时间
│ │
│ │ ⑧ 返回数据
│<────────────────────────│
│ {user: {...}} │3.2 Token 刷新流程 #
┌─────────────────────────────────────────────────────────────┐
│ Token 刷新流程 │
└─────────────────────────────────────────────────────────────┘
Access Token:有效期短(15-30分钟)
Refresh Token:有效期长(7-30天)
用户 服务器
│ │
│ 业务请求 │
│ Header: Bearer access_token
│────────────────────────>│
│ │
│ 返回:Token 过期 │
│<────────────────────────│
│ {code: 401, message: "token expired"}
│ │
│ 刷新 Token │
│ POST /refresh │
│ {refresh_token} │
│────────────────────────>│
│ │ 验证 refresh_token
│ │ 生成新的 access_token
│ │
│ 返回新的 Token │
│<────────────────────────│
│ {access_token, refresh_token}
│ │
│ 用新 Token 重新请求 │
│────────────────────────>│3.3 JWT 在请求中的传递 #
方式 1:Authorization Header(推荐)
GET /api/user HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// 注意:Bearer 前缀是标准写法方式 2:URL Query(不推荐)
GET /api/user?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... HTTP/1.1
// 问题:Token 会被记录在服务器日志、浏览器历史中方式 3:Cookie(可行但有限制)
GET /api/user HTTP/1.1
Host: api.example.com
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// 需要设置 HttpOnly 防止 XSS
// 但有 CSRF 风险四、Node.js 实现 #
4.1 安装依赖 #
# 安装 jsonwebtoken
npm install jsonwebtoken
# 安装 express(Web 框架)
npm install express
# 安装 bcryptjs(密码加密)
npm install bcryptjs4.2 基础示例:生成和验证 JWT #
// jwt-demo.js
const jwt = require('jsonwebtoken');
// 密钥(实际项目中应该放在环境变量)
const SECRET_KEY = 'your-secret-key';
// ==================== 生成 JWT ====================
// 用户数据(Payload)
const user = {
userId: '12345',
username: 'johndoe',
role: 'admin'
};
// 生成 Token
const token = jwt.sign(
user, // Payload
SECRET_KEY, // 密钥
{
expiresIn: '2h', // 过期时间:2小时
issuer: 'myapp.com', // 签发者
subject: 'user-auth' // 主题
}
);
console.log('生成的 Token:', token);
// ==================== 验证 JWT ====================
try {
// 验证 Token
const decoded = jwt.verify(token, SECRET_KEY);
console.log('解析结果:', decoded);
// 输出:
// {
// userId: '12345',
// username: 'johndoe',
// role: 'admin',
// iat: 1640000000, // 签发时间
// exp: 1640007200, // 过期时间
// iss: 'myapp.com',
// sub: 'user-auth'
// }
} catch (error) {
console.error('Token 验证失败:', error.message);
}
// ==================== 解码 JWT(不验证) ====================
// 只解码,不验证签名(用于查看内容)
const decodedWithoutVerify = jwt.decode(token);
console.log('解码结果(不验证):', decodedWithoutVerify);
// ==================== 过期时间格式 ====================
// expiresIn 支持多种格式
jwt.sign(user, SECRET_KEY, { expiresIn: '2h' }); // 2小时
jwt.sign(user, SECRET_KEY, { expiresIn: '2d' }); // 2天
jwt.sign(user, SECRET_KEY, { expiresIn: '7d' }); // 7天
jwt.sign(user, SECRET_KEY, { expiresIn: 3600 }); // 3600秒(1小时)
jwt.sign(user, SECRET_KEY, { expiresIn: '1y' }); // 1年(不推荐)4.3 Express 完整示例:登录认证 #
// server.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
// 配置
const SECRET_KEY = 'your-secret-key-should-be-in-env';
const JWT_EXPIRES_IN = '2h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
// 模拟数据库
const users = [
{
id: '1',
username: 'admin',
// 密码是 "123456" 的 hash
password: '$2a$10$YourHashedPasswordHere...',
role: 'admin'
},
{
id: '2',
username: 'user',
password: '$2a$10$YourHashedPasswordHere...',
role: 'user'
}
];
// 存储刷新令牌(实际应该用 Redis)
const refreshTokens = new Set();
// ==================== 工具函数 ====================
// 生成 Access Token
function generateAccessToken(user) {
return jwt.sign(
{
userId: user.id,
username: user.username,
role: user.role
},
SECRET_KEY,
{ expiresIn: JWT_EXPIRES_IN }
);
}
// 生成 Refresh Token
function generateRefreshToken(user) {
const token = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: REFRESH_TOKEN_EXPIRES_IN }
);
refreshTokens.add(token);
return token;
}
// 验证 JWT 中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
code: 401,
message: '缺少 Token'
});
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
code: 401,
message: 'Token 已过期'
});
}
if (err.name === 'JsonWebTokenError') {
return res.status(403).json({
code: 403,
message: 'Token 无效'
});
}
return res.status(403).json({
code: 403,
message: 'Token 验证失败'
});
}
req.user = user;
next();
});
}
// 权限检查中间件
function checkRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
code: 403,
message: '权限不足'
});
}
next();
};
}
// ==================== 路由 ====================
// 用户注册
app.post('/api/register', async (req, res) => {
const { username, password } = req.body;
// 检查用户是否存在
if (users.find(u => u.username === username)) {
return res.status(400).json({
code: 400,
message: '用户名已存在'
});
}
// 密码加密
const hashedPassword = await bcrypt.hash(password, 10);
// 创建用户
const newUser = {
id: String(users.length + 1),
username,
password: hashedPassword,
role: 'user'
};
users.push(newUser);
res.json({
code: 200,
message: '注册成功',
data: { userId: newUser.id }
});
});
// 用户登录
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({
code: 401,
message: '用户名或密码错误'
});
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
code: 401,
message: '用户名或密码错误'
});
}
// 生成 Token
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.json({
code: 200,
message: '登录成功',
data: {
accessToken,
refreshToken,
expiresIn: JWT_EXPIRES_IN,
user: {
id: user.id,
username: user.username,
role: user.role
}
}
});
});
// 刷新 Token
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
code: 400,
message: '缺少 refreshToken'
});
}
// 检查 refreshToken 是否有效
if (!refreshTokens.has(refreshToken)) {
return res.status(403).json({
code: 403,
message: '无效的 refreshToken'
});
}
jwt.verify(refreshToken, SECRET_KEY, (err, decoded) => {
if (err) {
refreshTokens.delete(refreshToken);
return res.status(403).json({
code: 403,
message: 'refreshToken 无效或已过期'
});
}
// 查找用户
const user = users.find(u => u.id === decoded.userId);
if (!user) {
return res.status(404).json({
code: 404,
message: '用户不存在'
});
}
// 生成新的 Token
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
// 删除旧的 refreshToken
refreshTokens.delete(refreshToken);
res.json({
code: 200,
message: '刷新成功',
data: {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: JWT_EXPIRES_IN
}
});
});
});
// 退出登录
app.post('/api/logout', (req, res) => {
const { refreshToken } = req.body;
if (refreshToken) {
refreshTokens.delete(refreshToken);
}
res.json({
code: 200,
message: '退出成功'
});
});
// 获取用户信息(需要认证)
app.get('/api/user', authenticateToken, (req, res) => {
res.json({
code: 200,
data: {
userId: req.user.userId,
username: req.user.username,
role: req.user.role
}
});
});
// 管理员接口(需要 admin 权限)
app.get('/api/admin/users', authenticateToken, checkRole('admin'), (req, res) => {
res.json({
code: 200,
data: users.map(u => ({
id: u.id,
username: u.username,
role: u.role
}))
});
});
// 启动服务器
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});4.4 前端使用示例 #
// api.js
const API_BASE_URL = 'http://localhost:3000';
// 存储 Token
function saveTokens(accessToken, refreshToken) {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}
// 获取 Token
function getAccessToken() {
return localStorage.getItem('accessToken');
}
// 清除 Token
function clearTokens() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
// 封装请求
async function request(url, options = {}) {
const accessToken = getAccessToken();
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const response = await fetch(`${API_BASE_URL}${url}`, {
...options,
headers,
});
const data = await response.json();
// Token 过期,尝试刷新
if (data.code === 401 && data.message === 'Token 已过期') {
const newToken = await refreshToken();
if (newToken) {
// 重试请求
headers['Authorization'] = `Bearer ${newToken}`;
const retryResponse = await fetch(`${API_BASE_URL}${url}`, {
...options,
headers,
});
return retryResponse.json();
} else {
// 刷新失败,跳转登录
clearTokens();
window.location.href = '/login';
}
}
return data;
}
// 刷新 Token
async function refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) return null;
try {
const response = await fetch(`${API_BASE_URL}/api/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const data = await response.json();
if (data.code === 200) {
saveTokens(data.data.accessToken, data.data.refreshToken);
return data.data.accessToken;
}
return null;
} catch (error) {
console.error('刷新 Token 失败:', error);
return null;
}
}
// 登录
async function login(username, password) {
const data = await request('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
if (data.code === 200) {
saveTokens(data.data.accessToken, data.data.refreshToken);
}
return data;
}
// 获取用户信息
async function getUser() {
return request('/api/user');
}
// 使用示例
login('admin', '123456').then(() => {
return getUser();
}).then(user => {
console.log('用户信息:', user);
});五、Java 实现 #
5.1 添加依赖 #
Maven:
<!-- pom.xml -->
<dependencies>
<!-- JWT 库:jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Gradle:
// build.gradle
dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-web'
}5.2 JWT 工具类 #
// JwtUtil.java
package com.example.demo.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
@Value("${jwt.secret:your-secret-key-must-be-at-least-256-bits-long}")
private String secret;
@Value("${jwt.expiration:7200000}") // 默认 2 小时
private Long expiration;
@Value("${jwt.refresh-expiration:604800000}") // 默认 7 天
private Long refreshExpiration;
// 生成密钥
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成 Access Token
*/
public String generateToken(String userId, String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
return Jwts.builder()
.setClaims(claims)
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 生成 Refresh Token
*/
public String generateRefreshToken(String userId) {
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
return null;
}
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
System.out.println("Token 已过期: " + e.getMessage());
} catch (UnsupportedJwtException e) {
System.out.println("不支持的 Token: " + e.getMessage());
} catch (MalformedJwtException e) {
System.out.println("Token 格式错误: " + e.getMessage());
} catch (SignatureException e) {
System.out.println("签名验证失败: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Token 为空: " + e.getMessage());
}
return false;
}
/**
* 检查 Token 是否过期
*/
public boolean isTokenExpired(String token) {
Claims claims = parseToken(token);
if (claims == null) return true;
return claims.getExpiration().before(new Date());
}
/**
* 从 Token 获取用户 ID
*/
public String getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims != null ? claims.getSubject() : null;
}
/**
* 从 Token 获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims != null ? (String) claims.get("username") : null;
}
/**
* 从 Token 获取角色
*/
public String getRoleFromToken(String token) {
Claims claims = parseToken(token);
return claims != null ? (String) claims.get("role") : null;
}
}5.3 Spring Boot 完整示例 #
// User.java
package com.example.demo.entity;
public class User {
private String id;
private String username;
private String password;
private String role;
// getter and setter
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
// LoginRequest.java
package com.example.demo.dto;
public class LoginRequest {
private String username;
private String password;
// getter and setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// LoginResponse.java
package com.example.demo.dto;
public class LoginResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private UserInfo user;
// getter and setter
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
public Long getExpiresIn() { return expiresIn; }
public void setExpiresIn(Long expiresIn) { this.expiresIn = expiresIn; }
public UserInfo getUser() { return user; }
public void setUser(UserInfo user) { this.user = user; }
public static class UserInfo {
private String id;
private String username;
private String role;
public UserInfo(String id, String username, String role) {
this.id = id;
this.username = username;
this.role = role;
}
// getter
public String getId() { return id; }
public String getUsername() { return username; }
public String getRole() { return role; }
}
}
// AuthController.java
package com.example.demo.controller;
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.LoginResponse;
import com.example.demo.entity.User;
import com.example.demo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
// 模拟数据库
private Map<String, User> userDatabase = new HashMap<>();
// 模拟 refreshToken 存储
private Map<String, String> refreshTokenStore = new HashMap<>();
// 初始化测试用户
public AuthController() {
User admin = new User();
admin.setId("1");
admin.setUsername("admin");
admin.setPassword("admin123"); // 实际应该加密
admin.setRole("admin");
userDatabase.put("admin", admin);
User user = new User();
user.setId("2");
user.setUsername("user");
user.setPassword("user123");
user.setRole("user");
userDatabase.put("user", user);
}
/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
User user = userDatabase.get(request.getUsername());
if (user == null || !user.getPassword().equals(request.getPassword())) {
return ResponseEntity.status(401).body(Map.of(
"code", 401,
"message", "用户名或密码错误"
));
}
// 生成 Token
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole());
String refreshToken = jwtUtil.generateRefreshToken(user.getId());
// 存储 refreshToken
refreshTokenStore.put(user.getId(), refreshToken);
// 构建响应
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(7200000L);
response.setUser(new LoginResponse.UserInfo(user.getId(), user.getUsername(), user.getRole()));
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "登录成功",
"data", response
));
}
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");
if (refreshToken == null || !jwtUtil.validateToken(refreshToken)) {
return ResponseEntity.status(403).body(Map.of(
"code", 403,
"message", "无效的 refreshToken"
));
}
String userId = jwtUtil.getUserIdFromToken(refreshToken);
// 验证 refreshToken 是否在存储中
if (!refreshToken.equals(refreshTokenStore.get(userId))) {
return ResponseEntity.status(403).body(Map.of(
"code", 403,
"message", "refreshToken 已失效"
));
}
// 查找用户
User user = null;
for (User u : userDatabase.values()) {
if (u.getId().equals(userId)) {
user = u;
break;
}
}
if (user == null) {
return ResponseEntity.status(404).body(Map.of(
"code", 404,
"message", "用户不存在"
));
}
// 生成新 Token
String newAccessToken = jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole());
String newRefreshToken = jwtUtil.generateRefreshToken(user.getId());
// 更新 refreshToken
refreshTokenStore.put(user.getId(), newRefreshToken);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "刷新成功",
"data", Map.of(
"accessToken", newAccessToken,
"refreshToken", newRefreshToken,
"expiresIn", 7200000L
)
));
}
/**
* 获取用户信息
*/
@GetMapping("/user")
public ResponseEntity<?> getUserInfo(@RequestHeader("Authorization") String authorization) {
// 提取 Token
String token = authorization.replace("Bearer ", "");
if (!jwtUtil.validateToken(token)) {
return ResponseEntity.status(401).body(Map.of(
"code", 401,
"message", "Token 无效或已过期"
));
}
String userId = jwtUtil.getUserIdFromToken(token);
String username = jwtUtil.getUsernameFromToken(token);
String role = jwtUtil.getRoleFromToken(token);
return ResponseEntity.ok(Map.of(
"code", 200,
"data", Map.of(
"userId", userId,
"username", username,
"role", role
)
));
}
/**
* 退出登录
*/
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");
if (refreshToken != null) {
String userId = jwtUtil.getUserIdFromToken(refreshToken);
refreshTokenStore.remove(userId);
}
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "退出成功"
));
}
}
// DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}5.4 application.yml 配置 #
# application.yml
server:
port: 8080
jwt:
secret: your-secret-key-must-be-at-least-256-bits-long-for-hs256
expiration: 7200000 # 2 小时(毫秒)
refresh-expiration: 604800000 # 7 天(毫秒)
spring:
application:
name: jwt-demo六、实战场景 #
场景 1:单点登录(SSO) #
// SSO 服务端
// 登录成功后,生成 Token,重定向到各子系统
// sso-server.js
app.post('/sso/login', async (req, res) => {
const { username, password, redirectUrl } = req.body;
// 验证用户
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ message: '认证失败' });
}
// 生成 Token
const token = jwt.sign(
{ userId: user.id, username: user.username },
SSO_SECRET,
{ expiresIn: '5m' } // 短有效期,仅用于一次跳转
);
// 重定向到子系统,携带 Token
res.redirect(`${redirectUrl}?token=${token}`);
});
// 子系统接收 Token
// subsystem.js
app.get('/sso/callback', async (req, res) => {
const { token } = req.query;
// 验证 SSO Token
try {
const decoded = jwt.verify(token, SSO_SECRET);
// 生成本系统 Token
const localToken = jwt.sign(
{ userId: decoded.userId },
LOCAL_SECRET,
{ expiresIn: '2h' }
);
// 设置本地 Token,完成登录
res.cookie('token', localToken, { httpOnly: true });
res.redirect('/dashboard');
} catch (error) {
res.status(401).send('Token 无效');
}
});场景 2:API 授权 #
// 第三方应用访问 API
// api-server.js
// 注册应用,获取 appId 和 appSecret
const apps = {
'app-001': {
id: 'app-001',
secret: 'secret-key-001',
permissions: ['read:user', 'read:order']
}
};
// 获取 Access Token
app.post('/oauth/token', (req, res) => {
const { appId, appSecret } = req.body;
const app = apps[appId];
if (!app || app.secret !== appSecret) {
return res.status(401).json({ message: '应用认证失败' });
}
// 生成 Token
const token = jwt.sign(
{
appId: app.id,
permissions: app.permissions
},
API_SECRET,
{ expiresIn: '1h' }
);
res.json({
access_token: token,
token_type: 'Bearer',
expires_in: 3600
});
});
// 权限检查中间件
function checkPermission(permission) {
return (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, API_SECRET);
if (!decoded.permissions.includes(permission)) {
return res.status(403).json({ message: '权限不足' });
}
req.app = decoded;
next();
} catch (error) {
res.status(401).json({ message: 'Token 无效' });
}
};
}
// API 路由
app.get('/api/users', checkPermission('read:user'), (req, res) => {
// 返回用户数据
res.json({ users: [...] });
});
app.get('/api/orders', checkPermission('read:order'), (req, res) => {
// 返回订单数据
res.json({ orders: [...] });
});场景 3:微服务间通信 #
// 微服务间调用时传递用户信息
// 服务 A 生成 Token
@RestController
public class ServiceAController {
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/call-service-b")
public String callServiceB(HttpServletRequest request) {
// 从请求中获取用户信息
String userToken = request.getHeader("Authorization");
Claims claims = jwtUtil.parseToken(userToken.replace("Bearer ", ""));
// 生成服务间 Token
String serviceToken = jwtUtil.generateToken(
claims.get("userId", String.class),
claims.get("username", String.class),
claims.get("role", String.class)
);
// 调用服务 B
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + serviceToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://service-b:8081/api/data",
HttpMethod.GET,
entity,
String.class
);
return response.getBody();
}
}
// 服务 B 验证 Token
@RestController
public class ServiceBController {
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/api/data")
public Map<String, Object> getData(@RequestHeader("Authorization") String authorization) {
String token = authorization.replace("Bearer ", "");
Claims claims = jwtUtil.parseToken(token);
return Map.of(
"userId", claims.get("userId"),
"username", claims.get("username"),
"data", "敏感数据"
);
}
}场景 4:WebSocket 认证 #
// WebSocket 连接时验证 JWT
const jwt = require('jsonwebtoken');
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 连接时验证 Token
wss.on('connection', (ws, req) => {
// 从 URL 参数获取 Token
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!token) {
ws.send(JSON.stringify({ error: '缺少 Token' }));
ws.close();
return;
}
try {
// 验证 Token
const decoded = jwt.verify(token, SECRET_KEY);
// 保存用户信息到 ws 对象
ws.userId = decoded.userId;
ws.username = decoded.username;
ws.send(JSON.stringify({ message: '连接成功' }));
// 处理消息
ws.on('message', (message) => {
console.log(`收到来自 ${ws.username} 的消息: ${message}`);
// 广播给所有连接
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
from: ws.username,
message: message.toString()
}));
}
});
});
} catch (error) {
ws.send(JSON.stringify({ error: 'Token 无效' }));
ws.close();
}
});
// 客户端连接
const ws = new WebSocket('ws://localhost:8080?token=xxx.yyy.zzz');七、安全最佳实践 #
7.1 密钥管理 #
// ❌ 错误:硬编码密钥
const SECRET = '123456';
// ✅ 正确:从环境变量读取
const SECRET = process.env.JWT_SECRET;
// ✅ 正确:密钥长度足够(HS256 至少 256 位)
const SECRET = 'your-secret-key-must-be-at-least-256-bits-long';
// ✅ 正确:使用非对称加密(更安全)
// 私钥签名,公钥验证
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');
// 生成 Token
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 验证 Token
const decoded = jwt.verify(token, publicKey);7.2 Token 有效期 #
// ✅ 合理的过期时间
const ACCESS_TOKEN_EXPIRES = '15m'; // Access Token: 15 分钟
const REFRESH_TOKEN_EXPIRES = '7d'; // Refresh Token: 7 天
// ❌ 错误:永不过期
const token = jwt.sign(payload, secret, { expiresIn: '100y' });
// ✅ 正确:根据场景设置
// 高安全场景(金融、支付)
const HIGH_SECURITY_EXPIRES = '5m';
// 普通场景(社交、内容)
const NORMAL_EXPIRES = '2h';
// 低安全场景(内容浏览)
const LOW_SECURITY_EXPIRES = '24h';7.3 Token 存储 #
// ❌ 错误:存储在 URL 中
window.location.href = '/dashboard?token=' + token;
// ❌ 错误:存储在 localStorage(有 XSS 风险)
localStorage.setItem('token', token);
// ✅ 较好:存储在 sessionStorage(关闭标签页即清除)
sessionStorage.setItem('token', token);
// ✅ 最好:存储在 HttpOnly Cookie
res.cookie('token', token, {
httpOnly: true, // 防止 JavaScript 访问(防 XSS)
secure: true, // 只在 HTTPS 下发送
sameSite: 'strict', // 防止 CSRF
maxAge: 7200000 // 2 小时
});
// 前端无需手动存储,浏览器自动携带
fetch('/api/user', {
credentials: 'include' // 携带 Cookie
});7.4 Token 注销 #
// JWT 无状态,无法主动失效
// 解决方案:维护黑名单或白名单
// 方案 1:黑名单(Redis)
const redis = require('redis');
const client = redis.createClient();
// 登出时加入黑名单
app.post('/logout', (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
// 解析 Token 获取过期时间
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
// 存入 Redis,过期时间与 Token 相同
client.setex(`blacklist:${token}`, ttl, '1');
res.json({ message: '登出成功' });
});
// 验证时检查黑名单
function verifyToken(token) {
// 先检查黑名单
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
throw new Error('Token 已失效');
}
// 再验证签名
return jwt.verify(token, SECRET);
}
// 方案 2:白名单(Redis)
// 每次登录生成唯一 jti,存入 Redis
// 验证时检查 jti 是否在白名单7.5 防止攻击 #
// 1. 防止 XSS
// - 不要在 localStorage 存 Token
// - 使用 HttpOnly Cookie
// - 对用户输入进行转义
// 2. 防止 CSRF
// - 使用 SameSite Cookie
// - 验证 CSRF Token
// - 使用 Authorization Header 而不是 Cookie
// 3. 防止 Token 泄露
// - 使用 HTTPS
// - 设置合理的过期时间
// - 敏感操作需要二次验证
// 4. 防止重放攻击
// 使用 jti(JWT ID)和 nonce
const token = jwt.sign(
{
userId: '123',
jti: crypto.randomUUID() // 唯一标识
},
SECRET,
{ expiresIn: '15m' }
);
// 服务端记录已使用的 jti
const usedJti = new Set();
function verifyWithReplayProtection(token) {
const decoded = jwt.verify(token, SECRET);
if (usedJti.has(decoded.jti)) {
throw new Error('Token 已使用');
}
usedJti.add(decoded.jti);
// 设置过期时间
setTimeout(() => {
usedJti.delete(decoded.jti);
}, decoded.exp * 1000 - Date.now());
return decoded;
}八、JWT vs Session #
8.1 对比表 #
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器 |
| 状态 | 无状态 | 有状态 |
| 扩展性 | 容易水平扩展 | 需要 Session 共享 |
| 跨域 | 友好 | 需要特殊处理 |
| 移动端 | 友好 | Cookie 不友好 |
| 安全性 | 需防 XSS | 需防 CSRF |
| 注销 | 困难(需要黑名单) | 简单(删除 Session) |
| 性能 | 每次都要验证签名 | 查询 Session(内存快) |
| 数据量 | Token 较大 | Session ID 很小 |
8.2 选择建议 #
使用 JWT 的场景:
- 前后端分离架构
- 移动端 App
- 微服务架构
- 单点登录(SSO)
- 第三方 API 授权
使用 Session 的场景:
- 传统单体应用
- 需要即时注销
- 敏感操作频繁
- 不需要跨域
- 服务器资源充足
8.3 混合方案 #
// 登录时同时使用 JWT 和 Session
app.post('/login', (req, res) => {
// 1. 创建 Session(用于敏感操作)
req.session.userId = user.id;
// 2. 生成 JWT(用于 API 认证)
const token = jwt.sign({ userId: user.id }, SECRET);
// 3. 返回 Token
res.json({
token,
message: '登录成功'
});
});
// 普通操作:用 JWT
app.get('/api/data', verifyJWT, (req, res) => {
// ...
});
// 敏感操作:用 Session
app.post('/api/change-password', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ message: '请先登录' });
}
// ...
});九、常见问题 #
Q1:JWT 被盗用怎么办? #
防范措施:
- 短有效期 - Access Token 15-30 分钟
- HTTPS - 防止中间人攻击
- HttpOnly Cookie - 防止 XSS
- 刷新机制 - Refresh Token 轮换
- IP 绑定 - Token 与 IP 绑定(可选)
- 设备绑定 - Token 与设备绑定(可选)
// IP 绑定示例
const token = jwt.sign(
{
userId: user.id,
ip: req.ip // 记录登录 IP
},
SECRET
);
// 验证时检查 IP
function verifyToken(token, currentIp) {
const decoded = jwt.verify(token, SECRET);
if (decoded.ip !== currentIp) {
throw new Error('IP 地址已变化');
}
return decoded;
}Q2:JWT 过期后如何处理? #
// 前端处理
async function request(url, options) {
const response = await fetch(url, options);
if (response.status === 401) {
const data = await response.json();
if (data.message === 'Token 已过期') {
// 尝试刷新
const newToken = await refreshToken();
if (newToken) {
// 重试请求
options.headers.Authorization = `Bearer ${newToken}`;
return fetch(url, options);
} else {
// 跳转登录
window.location.href = '/login';
}
}
}
return response;
}Q3:JWT 可以存储敏感信息吗? #
不可以!
// ❌ 危险:存储密码
const token = jwt.sign({ password: '123456' }, SECRET);
// JWT 只是 Base64 编码,不是加密!
// 任何人都可以解码看到内容
// 解码 Payload(不需要密钥)
const payload = JSON.parse(atob(token.split('.')[1]));
console.log(payload); // { password: '123456' }Q4:如何实现"记住我"功能? #
// 登录时根据选项设置不同的过期时间
app.post('/login', (req, res) => {
const { username, password, rememberMe } = req.body;
// ...
// 根据"记住我"设置不同的过期时间
const expiresIn = rememberMe ? '30d' : '2h';
const token = jwt.sign(
{ userId: user.id },
SECRET,
{ expiresIn }
);
res.json({ token, expiresIn });
});Q5:JWT 能用于移动端吗? #
可以,这是 JWT 的优势之一。
// 移动端存储
// iOS:Keychain
// Android:SharedPreferences(加密)或 Keystore
// 请求示例(iOS Swift)
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")Q6:如何处理并发 Token 刷新? #
// 问题:多个请求同时发现 Token 过期,都去刷新
// 解决:加锁,只刷新一次
let isRefreshing = false;
let refreshSubscribers = [];
async function refreshToken() {
if (isRefreshing) {
// 等待刷新完成
return new Promise(resolve => {
refreshSubscribers.push(resolve);
});
}
isRefreshing = true;
try {
const response = await fetch('/api/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
const newToken = data.data.accessToken;
// 通知所有等待的请求
refreshSubscribers.forEach(callback => callback(newToken));
refreshSubscribers = [];
return newToken;
} finally {
isRefreshing = false;
}
}Q7:JWT 的 Payload 大小有限制吗? #
没有严格限制,但建议小于 4KB。
// 问题:Token 太大
const bigData = { /* ... 10KB 数据 ... */ };
const token = jwt.sign(bigData, SECRET);
// 结果:Token 字符串很长,HTTP 请求体积大
// 建议:只存必要的标识
const token = jwt.sign({ userId: '123' }, SECRET);
// 其他数据从数据库查询十、总结速记 #
JWT 三部分 #
Header.Payload.Signature
Header: 算法和类型
Payload: 用户数据(Base64,不加密)
Signature: 防伪签名Node.js 快速使用 #
const jwt = require('jsonwebtoken');
// 生成
const token = jwt.sign({ userId: '123' }, SECRET, { expiresIn: '2h' });
// 验证
const decoded = jwt.verify(token, SECRET);
// 解码(不验证)
const payload = jwt.decode(token);Java 快速使用 #
// 生成
String token = Jwts.builder()
.setSubject("123")
.setExpiration(new Date(System.currentTimeMillis() + 7200000))
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256)
.compact();
// 验证
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();安全要点 #
1. 密钥要长:至少 256 位
2. 有效期短:Access Token 15-30 分钟
3. HTTPS:防止传输泄露
4. 不存敏感:Payload 不加密
5. 存储:HttpOnly Cookie 优于 localStorage
6. 刷新:使用 Refresh TokenJWT vs Session 选择 #
JWT:
- 分布式、微服务
- 移动端
- SSO
- API 授权
Session:
- 单体应用
- 敏感操作
- 即时注销典型流程 #
1. 登录 → 返回 JWT
2. 存储 Token(Cookie/localStorage)
3. 请求携带 Token(Authorization: Bearer xxx)
4. 服务器验证签名 + 检查过期
5. 过期 → 刷新 Token最后更新:2026-03-29