JWT 权限认证
约 1351 字大约 5 分钟
JWT认证权限
2026-04-16
概述
Session vs JWT
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务端(内存/Redis) | 客户端(Token) |
| 扩展性 | 分布式需共享 Session | 无状态,自包含 |
| 跨域 | 需要特殊处理 | 天然支持跨域 |
| 占用空间 | 需存储会话数据 | 紧凑但有冗余 header |
| 失效机制 | 服务端直接删除 | 需配合黑名单或短期 exp |
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。
JWT 结构
一个 JWT 由三部分组成,用点号分隔:xxxxx.yyyyy.zzzzz
Header.Payload.SignatureHeader
{
"alg": "HS256", // 签名算法
"typ": "JWT" // 类型
}Base64 编码后:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
{
"sub": "1234567890", // 用户 ID
"name": "John Doe", // 自定义 Claims
"role": "admin",
"iat": 1516239022, // 签发时间
"exp": 1516242622 // 过期时间
}Base64 编码后:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDk1MjJ9
Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)最终生成:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Base64 编解码
// Node.js 环境
const base64Str = Buffer.from(JSON.stringify({ a: 1 }))
.toString('base64');
const decoded = JSON.parse(
Buffer.from(base64Str, 'base64').toString()
);JWT 工作流程
┌─────────┐ ┌─────────────┐
│ User │ │ Server │
└────┬────┘ └──────┬──────┘
│ │
│ 1. 登录 (POST /login) │
│ ────────────────────────────────► │
│ │
│ 2. 验证用户名密码 │
│ │
│ 3. 签发 JWT Token │
│ ◄──────────────────────────────── │
│ │
│ 4. 携带 Token 访问受保护资源 │
│ Authorization: Bearer <token>│
│ ────────────────────────────────► │
│ │
│ 5. 验证 Token │
│ │
│ 6. 返回资源 │
│ ◄──────────────────────────────── │实现方案
签发 (Sign)
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
role: string;
}
const SECRET = process.env.JWT_SECRET!;
const EXPIRES_IN = '2h';
function generateToken(payload: TokenPayload): string {
return jwt.sign(payload, SECRET, {
expiresIn: EXPIRES_IN,
issuer: 'my-app',
audience: 'my-app-users'
});
}
// 生成 Refresh Token(更长有效期)
function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, SECRET, {
expiresIn: '7d'
});
}验证 (Verify)
import jwt from 'jsonwebtoken';
function verifyToken(token: string): TokenPayload | null {
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'my-app',
audience: 'my-app-users'
});
return decoded as TokenPayload;
} catch (err) {
// token 无效或已过期
return null;
}
}
// Express/Koa 中间件
function authMiddleware(req: any, res: any, next: any) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供 Token' });
}
const token = authHeader.slice(7);
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Token 无效或已过期' });
}
req.user = decoded;
next();
}刷新 (Refresh Token)
interface RefreshTokenStore {
[userId: string]: string; // userId -> refreshToken
}
const refreshTokens: RefreshTokenStore = {};
// 刷新 Token
function refreshAccessToken(refreshToken: string): string | null {
try {
const decoded = jwt.verify(refreshToken, SECRET) as TokenPayload;
// 检查 refreshToken 是否与存储的匹配
if (refreshTokens[decoded.userId] !== refreshToken) {
return null; // refreshToken 已被更换
}
return generateToken({
userId: decoded.userId,
role: decoded.role
});
} catch {
return null;
}
}安全注意
有效期设置 (exp)
- Access Token: 建议 15 分钟 ~ 2 小时
- Refresh Token: 建议 7 天 ~ 30 天
- 不要设置过长的有效期,宁可频繁刷新
// 短期 token 示例
jwt.sign({ userId }, SECRET, { expiresIn: '15m' });
// 长期 token(Refresh Token)示例
jwt.sign({ userId }, SECRET, { expiresIn: '7d' });黑名单机制
import redis from 'ioredis';
const redisClient = new redis();
// 加入黑名单(退出登录时)
async function blacklistToken(token: string, expInSec: number) {
await redisClient.setex(`blacklist:${token}`, expInSec, '1');
}
// 检查黑名单
async function isBlacklisted(token: string): Promise<boolean> {
const result = await redisClient.get(`blacklist:${token}`);
return result === '1';
}
// 中间件中检查
async function authMiddleware(req: any, res: any, next: any) {
const token = req.headers.authorization?.slice(7);
if (await isBlacklisted(token)) {
return res.status(401).json({ error: 'Token 已失效' });
}
// ... 继续验证
}HTTPS Only
生产环境必须使用 HTTPS,防止 Token 在传输过程中被窃取:
# Nginx 配置
proxy_set_header X-Forwarded-Proto https;Secret 管理
// 禁止硬编码
const SECRET = process.env.JWT_SECRET;
// 生成强随机密钥
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
// 密钥轮换策略
const SECRET_V1 = process.env.JWT_SECRET_V1;
const SECRET_V2 = process.env.JWT_SECRET_V2;
function verifyWithRotation(token: string) {
try {
return jwt.verify(token, SECRET_V2);
} catch {
// 尝试旧密钥
return jwt.verify(token, SECRET_V1);
}
}前端配合
localStorage vs httpOnly Cookie
| 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| localStorage | 随时访问,方便控制 | 易受 XSS 攻击 | 单页应用 |
| httpOnly Cookie | 无法被 JS 访问,防 XSS | 依赖 Cookie 机制 | 传统 Web 应用 |
token 携带方式
// 方式 1: Authorization Header(推荐)
fetch('/api/user', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
// 方式 2: Cookie(自动发送)
// 服务端设置 httpOnly cookie,前端无需处理Token 刷新策略
// 响应拦截器中处理 401,自动刷新
async function fetchWithAutoRefresh(url, options) {
const response = await fetch(url, options);
if (response.status === 401) {
// 尝试刷新 Token
const refreshResponse = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // 携带 refreshToken cookie
});
if (refreshResponse.ok) {
// 刷新成功,重试原请求
return fetch(url, options);
} else {
// 刷新失败,重新登录
window.location.href = '/login';
}
}
return response;
}实战示例
Express 中间件
import express, { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const app = express();
interface AuthRequest extends Request {
user?: {
userId: string;
role: string;
};
}
// JWT 验证中间件
function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: '缺少认证 Token' });
}
const token = authHeader.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
userId: string;
role: string;
};
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Token 无效或已过期' });
}
}
// 登录接口
app.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
// 验证用户...(省略)
const token = jwt.sign(
{ userId: '123', role: 'admin' },
process.env.JWT_SECRET!,
{ expiresIn: '2h' }
);
res.json({ token });
});
// 受保护的路由
app.get('/api/user', authenticate, (req: AuthRequest, res: Response) => {
res.json({ userId: req.user!.userId, role: req.user!.role });
});
app.listen(3000);Koa 中间件
import Koa from 'koa';
import Router from '@koa/router';
import jwt from 'jsonwebtoken';
const app = new Koa();
const router = new Router();
// JWT 验证中间件
function authenticate(ctx: any, next: any) {
const authHeader = ctx.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
ctx.status = 401;
ctx.body = { error: '缺少认证 Token' };
return;
}
const token = authHeader.slice(7);
try {
ctx.state.user = jwt.verify(token, process.env.JWT_SECRET!);
return next();
} catch (err) {
ctx.status = 401;
ctx.body = { error: 'Token 无效或已过期' };
}
}
router.post('/login', (ctx) => {
// 验证用户...(省略)
const token = jwt.sign(
{ userId: '123', role: 'admin' },
process.env.JWT_SECRET!,
{ expiresIn: '2h' }
);
ctx.body = { token };
});
router.get('/api/user', authenticate, (ctx) => {
ctx.body = ctx.state.user;
});
app.use(router.routes());
app.listen(3000);总结
- 选择合适的 Token 有效期:短期 token 安全性更高,配合 refresh token 实现续期
- 使用 HTTPS:生产环境必须启用
- 保护好 Secret:使用环境变量,定期轮换
- 实现黑名单机制:支持主动失效 token
- 前端注意 XSS 防护:避免在 localStorage 中存储敏感信息
- 考虑密钥轮换:支持多版本密钥平滑过渡
