Appearance
JWT 认证原理 - 现代Web认证方案
1. JWT 简介
1.1 什么是 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。这些信息可以被验证和信任,因为它是数字签名的。
核心特点:
- 自包含:包含所有必要的用户信息
- 无状态:服务器不需要存储会话
- 可扩展:易于在分布式系统中使用
- 跨域友好:不依赖Cookie
1.2 JWT 结构
JWT由三部分组成,用点(.)分隔:
Header.Payload.SignatureHeader(头部)
json
{
"alg": "HS256", // 签名算法
"typ": "JWT" // 令牌类型
}Payload(载荷)
json
{
// 标准声明
"iss": "issuer", // 签发者
"sub": "user123", // 主题(用户ID)
"aud": "audience", // 受众
"exp": 1735689600, // 过期时间
"nbf": 1735603200, // 生效时间
"iat": 1735603200, // 签发时间
"jti": "unique-id", // JWT ID
// 自定义声明
"username": "john",
"email": "john@example.com",
"role": "admin"
}Signature(签名)
javascript
// HMAC SHA256
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)1.3 完整示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
┌─────────────────── Header ────────────────────┐
│ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 │
│ {"alg":"HS256","typ":"JWT"} │
└───────────────────────────────────────────────┘
┌─────────────────── Payload ───────────────────┐
│ eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ │
│ {"sub":"user123","name":"John Doe","iat":1516239022} │
└───────────────────────────────────────────────┘
┌─────────────────── Signature ─────────────────┐
│ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c │
│ HMACSHA256(header.payload, secret) │
└───────────────────────────────────────────────┘2. JWT 认证流程
2.1 基本流程
┌─────┐ ┌─────────┐
│ │ 1. 登录(用户名/密码) │ │
│ │ ─────────────────────→ │ │
│ │ │ │
│ │ 2. 验证成功,返回JWT │ │
│ │ ←───────────────────── │ │
│ │ │ │
│ │ 3. 携带JWT访问API │ │
│用户 │ ─────────────────────→ │ 服务器 │
│ │ │ │
│ │ 4. 验证JWT,返回数据 │ │
│ │ ←───────────────────── │ │
│ │ │ │
│ │ 5. 后续请求携带JWT │ │
│ │ ─────────────────────→ │ │
└─────┘ └─────────┘2.2 详细步骤
步骤1:用户登录
typescript
// 客户端
const loginRequest = {
username: 'john',
password: 'secret123'
};
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(loginRequest)
});
const { token } = await response.json();步骤2:服务器验证并生成JWT
typescript
// 服务器
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
async function login(username: string, password: string) {
// 验证用户
const user = await getUserByUsername(username);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new Error('Invalid credentials');
}
// 生成JWT
const token = jwt.sign(
{
sub: user.id,
username: user.username,
role: user.role
},
process.env.JWT_SECRET!,
{ expiresIn: '1h' }
);
return { token };
}步骤3:客户端存储JWT
typescript
// 存储到localStorage(不推荐,XSS风险)
localStorage.setItem('token', token);
// 存储到sessionStorage
sessionStorage.setItem('token', token);
// 存储到内存(推荐)
let authToken: string | null = token;
// 存储到HttpOnly Cookie(最安全,由服务器设置)
// 不需要客户端代码步骤4:发送请求时携带JWT
typescript
// Authorization header(推荐)
const response = await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// 自定义header
const response = await fetch('/api/protected', {
headers: {
'X-Auth-Token': token
}
});
// Cookie(自动发送)
const response = await fetch('/api/protected', {
credentials: 'include'
});步骤5:服务器验证JWT
typescript
// 中间件验证
import { Request, Response, NextFunction } from 'express';
function authenticateToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any;
req.user = payload;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// 使用中间件
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
});
});3. JWT 实现
3.1 Node.js 实现
bash
# 安装依赖
npm install jsonwebtoken bcrypt
npm install -D @types/jsonwebtoken @types/bcrypt生成JWT
typescript
// auth.service.ts
import jwt from 'jsonwebtoken';
interface TokenPayload {
sub: string;
username: string;
role: string;
}
export class AuthService {
private static readonly SECRET = process.env.JWT_SECRET!;
private static readonly REFRESH_SECRET = process.env.REFRESH_SECRET!;
// 生成访问令牌
static generateAccessToken(payload: TokenPayload): string {
return jwt.sign(
payload,
this.SECRET,
{
expiresIn: '15m',
issuer: 'my-app',
audience: 'my-app-users'
}
);
}
// 生成刷新令牌
static generateRefreshToken(userId: string): string {
return jwt.sign(
{ sub: userId },
this.REFRESH_SECRET,
{ expiresIn: '7d' }
);
}
// 验证令牌
static verifyAccessToken(token: string): TokenPayload {
return jwt.verify(token, this.SECRET) as TokenPayload;
}
static verifyRefreshToken(token: string): { sub: string } {
return jwt.verify(token, this.REFRESH_SECRET) as { sub: string };
}
}登录端点
typescript
// auth.controller.ts
import express from 'express';
import bcrypt from 'bcrypt';
import { AuthService } from './auth.service';
const router = express.Router();
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 查找用户
const user = await getUserByUsername(username);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 验证密码
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 生成令牌
const accessToken = AuthService.generateAccessToken({
sub: user.id,
username: user.username,
role: user.role
});
const refreshToken = AuthService.generateRefreshToken(user.id);
// 设置HttpOnly Cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
});
res.json({ accessToken });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;认证中间件
typescript
// auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AuthService } from './auth.service';
export interface AuthRequest extends Request {
user?: {
sub: string;
username: string;
role: string;
};
}
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
try {
// 从header获取token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
// 验证token
const payload = AuthService.verifyAccessToken(token);
// 附加用户信息到请求
req.user = payload;
next();
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
return res.status(403).json({ error: 'Invalid token' });
}
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expired' });
}
res.status(500).json({ error: 'Authentication failed' });
}
}
// 角色检查中间件
export function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}刷新令牌
typescript
// auth.controller.ts
router.post('/refresh', async (req, res) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// 验证刷新令牌
const payload = AuthService.verifyRefreshToken(refreshToken);
// 获取用户信息
const user = await getUserById(payload.sub);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// 生成新的访问令牌
const accessToken = AuthService.generateAccessToken({
sub: user.id,
username: user.username,
role: user.role
});
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// 登出
router.post('/logout', (req, res) => {
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});3.2 React 集成
创建 Auth Context
tsx
// AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import axios from 'axios';
interface User {
id: string;
username: string;
role: string;
}
interface AuthContextType {
user: User | null;
token: string | null;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// 从sessionStorage恢复token
const storedToken = sessionStorage.getItem('token');
if (storedToken) {
setToken(storedToken);
fetchUser(storedToken);
}
}, []);
const fetchUser = async (token: string) => {
try {
const response = await axios.get('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
setUser(response.data);
} catch (error) {
// Token无效,清除
setToken(null);
sessionStorage.removeItem('token');
}
};
const login = async (username: string, password: string) => {
const response = await axios.post('/api/login', {
username,
password
});
const { accessToken } = response.data;
setToken(accessToken);
sessionStorage.setItem('token', accessToken);
await fetchUser(accessToken);
};
const logout = () => {
setToken(null);
setUser(null);
sessionStorage.removeItem('token');
// 通知服务器
axios.post('/api/logout');
};
return (
<AuthContext.Provider value={{
user,
token,
login,
logout,
isAuthenticated: !!token
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}配置 Axios 拦截器
typescript
// api/client.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: '/api',
timeout: 10000
});
// 请求拦截器 - 添加token
apiClient.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 处理token过期
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Token过期,尝试刷新
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const response = await axios.post('/api/refresh', {}, {
withCredentials: true
});
const { accessToken } = response.data;
sessionStorage.setItem('token', accessToken);
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
// 刷新失败,清除token并跳转登录
sessionStorage.removeItem('token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default apiClient;受保护的路由
tsx
// ProtectedRoute.tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './AuthContext';
export function ProtectedRoute() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
// 角色保护
export function RoleProtectedRoute({ roles }: { roles: string[] }) {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (user && !roles.includes(user.role)) {
return <Navigate to="/forbidden" replace />;
}
return <Outlet />;
}tsx
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import { ProtectedRoute, RoleProtectedRoute } from './ProtectedRoute';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
{/* 需要认证 */}
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
{/* 需要管理员角色 */}
<Route element={<RoleProtectedRoute roles={['admin']} />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}登录表单
tsx
// LoginPage.tsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
export function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
await login(username, password);
navigate('/dashboard');
} catch (err: any) {
setError(err.response?.data?.error || 'Login failed');
}
};
return (
<div className="login-page">
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{error && <div className="error">{error}</div>}
<div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
required
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
</div>
<button type="submit">Login</button>
</form>
</div>
);
}4. JWT 最佳实践
4.1 安全存储
typescript
// ❌ 不安全 - localStorage (XSS风险)
localStorage.setItem('token', token);
// ❌ 不安全 - 普通Cookie (CSRF风险)
document.cookie = `token=${token}`;
// ✅ 推荐 - HttpOnly Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
// ✅ 推荐 - 内存存储(单页应用)
class TokenStore {
private token: string | null = null;
setToken(token: string) {
this.token = token;
}
getToken(): string | null {
return this.token;
}
clearToken() {
this.token = null;
}
}4.2 令牌过期策略
typescript
// 双令牌策略
interface TokenPair {
accessToken: string; // 短期(15分钟)
refreshToken: string; // 长期(7天)
}
function generateTokenPair(userId: string): TokenPair {
const accessToken = jwt.sign(
{ sub: userId },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// 刷新策略
async function refreshAccessToken(refreshToken: string) {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: payload.sub },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
return newAccessToken;
}4.3 令牌撤销
typescript
// 使用黑名单
const tokenBlacklist = new Set<string>();
function revokeToken(token: string) {
tokenBlacklist.add(token);
}
function isTokenRevoked(token: string): boolean {
return tokenBlacklist.has(token);
}
// 验证时检查黑名单
function verifyToken(token: string) {
if (isTokenRevoked(token)) {
throw new Error('Token revoked');
}
return jwt.verify(token, SECRET);
}
// 使用Redis存储黑名单
import Redis from 'ioredis';
const redis = new Redis();
async function revokeToken(token: string) {
const payload = jwt.decode(token) as any;
const ttl = payload.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blacklist:${token}`, ttl, '1');
}
}
async function isTokenRevoked(token: string): Promise<boolean> {
const exists = await redis.exists(`blacklist:${token}`);
return exists === 1;
}4.4 敏感信息处理
typescript
// ❌ 不要在payload中存储敏感信息
const badToken = jwt.sign({
password: 'secret123', // 永远不要
creditCard: '1234-5678-9012', // 永远不要
ssn: '123-45-6789' // 永远不要
}, SECRET);
// ✅ 只存储必要的标识信息
const goodToken = jwt.sign({
sub: userId,
role: userRole,
iat: Math.floor(Date.now() / 1000)
}, SECRET, {
expiresIn: '1h'
});
// 需要详细信息时从数据库查询
async function getUserData(userId: string) {
const user = await db.users.findById(userId);
return {
id: user.id,
username: user.username,
email: user.email,
// 其他非敏感信息
};
}5. 常见安全问题
5.1 XSS攻击
typescript
// 防御措施
// 1. 不要在localStorage存储token
// 2. 使用HttpOnly Cookie
// 3. 实施CSP策略
// 4. 转义用户输入
// 设置安全的Cookie
res.cookie('token', token, {
httpOnly: true, // 防止JavaScript访问
secure: true, // 只在HTTPS下发送
sameSite: 'strict', // CSRF保护
maxAge: 3600000 // 1小时
});5.2 CSRF攻击
typescript
// 防御措施
// 1. 使用SameSite Cookie
// 2. 实施CSRF Token
// 3. 验证Origin/Referer
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://example.com'];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
});5.3 重放攻击
typescript
// 防御措施
// 1. 使用短期token
// 2. 添加jti(JWT ID)
// 3. 实施nonce机制
function generateToken(userId: string) {
const jti = crypto.randomBytes(16).toString('hex');
return jwt.sign({
sub: userId,
jti: jti
}, SECRET, {
expiresIn: '15m'
});
}
// 验证时检查jti是否已使用
const usedJtis = new Set<string>();
function verifyToken(token: string) {
const payload = jwt.verify(token, SECRET) as any;
if (usedJtis.has(payload.jti)) {
throw new Error('Token已被使用');
}
usedJtis.add(payload.jti);
return payload;
}6. JWT vs Session
6.1 对比
typescript
// Session认证
/**
* 优点:
* - 服务器可控制会话
* - 易于撤销
* - 更安全(信息存服务器)
*
* 缺点:
* - 需要服务器存储
* - 难以扩展
* - CORS问题
*/
// JWT认证
/**
* 优点:
* - 无状态,易扩展
* - 跨域友好
* - 减少数据库查询
*
* 缺点:
* - 难以撤销
* - payload大小限制
* - 无法更新未过期token
*/6.2 选择建议
typescript
// 使用Session的场景
- 需要频繁撤销会话
- 高安全性要求
- 单体应用
// 使用JWT的场景
- 微服务架构
- 移动应用
- 第三方API集成
- 需要跨域认证7. 实战技巧
7.1 Token刷新机制
typescript
// 自动刷新策略
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
private refreshTimer: NodeJS.Timeout | null = null;
setTokens(access: string, refresh: string) {
this.accessToken = access;
this.refreshToken = refresh;
this.scheduleRefresh();
}
private scheduleRefresh() {
// 解析token获取过期时间
const payload = jwt.decode(this.accessToken!) as any;
const expiresIn = payload.exp * 1000 - Date.now();
// 在过期前5分钟刷新
const refreshIn = expiresIn - 5 * 60 * 1000;
if (refreshIn > 0) {
this.refreshTimer = setTimeout(() => {
this.refresh();
}, refreshIn);
}
}
private async refresh() {
try {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
const { accessToken } = await response.json();
this.accessToken = accessToken;
this.scheduleRefresh();
} catch (error) {
// 刷新失败,跳转登录
window.location.href = '/login';
}
}
getAccessToken() {
return this.accessToken;
}
clear() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.accessToken = null;
this.refreshToken = null;
}
}7.2 多设备管理
typescript
// 记录登录设备
interface Session {
userId: string;
deviceId: string;
refreshToken: string;
userAgent: string;
ipAddress: string;
createdAt: Date;
}
// 登录时创建会话
async function createSession(userId: string, req: Request) {
const session: Session = {
userId,
deviceId: generateDeviceId(req),
refreshToken: generateRefreshToken(userId),
userAgent: req.headers['user-agent'] || '',
ipAddress: req.ip,
createdAt: new Date()
};
await db.sessions.create(session);
return session;
}
// 撤销特定设备
async function revokeDevice(userId: string, deviceId: string) {
await db.sessions.deleteOne({ userId, deviceId });
}
// 撤销所有设备(登出所有)
async function revokeAllDevices(userId: string) {
await db.sessions.deleteMany({ userId });
}8. 测试
8.1 单元测试
typescript
// auth.service.test.ts
import { AuthService } from './auth.service';
import jwt from 'jsonwebtoken';
describe('AuthService', () => {
describe('generateAccessToken', () => {
it('should generate valid token', () => {
const payload = {
sub: 'user123',
username: 'john',
role: 'user'
};
const token = AuthService.generateAccessToken(payload);
const decoded = jwt.decode(token) as any;
expect(decoded.sub).toBe('user123');
expect(decoded.username).toBe('john');
expect(decoded.role).toBe('user');
});
it('should set correct expiration', () => {
const token = AuthService.generateAccessToken({
sub: 'user123',
username: 'john',
role: 'user'
});
const decoded = jwt.decode(token) as any;
const expiresIn = decoded.exp - decoded.iat;
expect(expiresIn).toBe(15 * 60); // 15分钟
});
});
describe('verifyAccessToken', () => {
it('should verify valid token', () => {
const token = AuthService.generateAccessToken({
sub: 'user123',
username: 'john',
role: 'user'
});
const payload = AuthService.verifyAccessToken(token);
expect(payload.sub).toBe('user123');
});
it('should reject expired token', () => {
const token = jwt.sign(
{ sub: 'user123' },
process.env.JWT_SECRET!,
{ expiresIn: '-1s' }
);
expect(() => {
AuthService.verifyAccessToken(token);
}).toThrow();
});
});
});8.2 集成测试
typescript
// auth.e2e.test.ts
import request from 'supertest';
import app from '../app';
describe('Authentication E2E', () => {
it('should login and access protected route', async () => {
// 登录
const loginResponse = await request(app)
.post('/api/login')
.send({
username: 'testuser',
password: 'testpass'
});
expect(loginResponse.status).toBe(200);
expect(loginResponse.body.accessToken).toBeDefined();
const { accessToken } = loginResponse.body;
// 访问受保护路由
const protectedResponse = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${accessToken}`);
expect(protectedResponse.status).toBe(200);
});
it('should refresh expired token', async () => {
// 登录获取token
const loginResponse = await request(app)
.post('/api/login')
.send({ username: 'testuser', password: 'testpass' });
const cookies = loginResponse.headers['set-cookie'];
// 刷新token
const refreshResponse = await request(app)
.post('/api/refresh')
.set('Cookie', cookies);
expect(refreshResponse.status).toBe(200);
expect(refreshResponse.body.accessToken).toBeDefined();
});
});9. 总结
JWT认证的关键要点:
- 安全存储: 使用HttpOnly Cookie或内存存储,避免localStorage
- 短期有效: accessToken使用短期有效期(15分钟)
- 刷新机制: 使用refreshToken实现无感刷新
- 撤销策略: 实施token黑名单或会话管理
- 防御措施: 防范XSS、CSRF、重放攻击
通过正确实施JWT认证,可以构建安全、可扩展的身份验证系统。