认证与授权
What — 是什么
认证(Authentication)验证”你是谁”,授权(Authorization)决定”你能做什么”。两者是 Web 安全的基石,在 Node.js 应用中通常通过 Session/Cookie、JWT、OAuth2.0 等机制实现。
核心概念:
- Session/Cookie 认证:服务端创建 Session 存储 用户状态,通过 Cookie 中的 Session ID 关联客户端,是有状态认证的经典方案
- JWT(JSON Web Token):由 Header(算法与类型)、Payload(声明数据)、Signature(签名)三部分组成的无状态令牌,自包含用户信息,适合分布式系统
- OAuth 2.0:开放授权协议,允许用户授权第三方应用访问其资源,而不暴露密码,支持授权码、隐式、密码、客户端凭证四种授权模式
- Passport.js:Node.js 最流行的认证中间件,提供 500+ 策略(Strategy)支持各种认证方式,基于策略模式设计
- 刷新令牌(Refresh Token):长期有效的令牌,用于在访问令牌过期后获取新的访问令牌,避免用户频繁重新登录
- RBAC(基于角色的访问控制):通过角色分配权限,用户关联角色从而获得权限,是最常用的授权模型
- ABAC(基于属性的访问控制):根据用户属性、资源属性、环境属性等动态决策,比 RBAC 更细粒度
- CORS 认证:跨域资源共享中,携带 Cookie 的请求需要
credentials: include,服务端需设置Access-Control-Allow-Credentials: true且不允许通配符 Origin - CSRF Token:防范跨站请求伪造的令牌机制,服务端生成随机 Token 嵌入表单,提交时验证一致性
关键特性:
- 认证与授权是两个独立但紧密关联的过程:先认证身份,再授权操作
- JWT 签名算法:HS256(对称密钥 HMAC)、RS256(非对称 RSA)、ES256(椭圆曲线),生产环境推荐 RS256
- OAuth 2.0 授权码模式是安全性最高的流程,适合有后端的 Web 应用
- Passport.js 的策略机制:每种认证方式是一个 Strategy,通过
passport.use()注册,passport.authenticate()调用
核心架构:
- 认证流程:客户端提交凭证 → 服务端验证 → 生成令牌/Session → 返回客户端 → 后续请求携带凭证
- 授权流程:请求到达 → 提取用户身份 → 查询权限 → 判断是否允许 → 执行或拒绝
- JWT 数据流:登录 → 服务端签发 Access Token + Refresh Token → 客户端存储 → 请求携带 Access Token → 过期后用 Refresh Token 刷新
- OAuth 2.0 授权码流程:用户点击授权 → 跳转授权服务器 → 用户同意 → 回调携带授权码 → 后端用授权码换令牌
Why — 为什么
适用场景:
- 单体应用:Session/Cookie 认证最简单直接,服务端本地存储 Session,无需额外基础设施
- 前后端分离:JWT 无状态特性更适合,前端存储 Token 携带在 Authorization 头中,避免跨域 Cookie 问题
- 第三方登录:OAuth 2.0 是标准协议,支持 Google、GitHub、微信等第三方平台登录
- 微服务架构:JWT 自包含用户信息,各服务独立验证无需共享 Session 存储,天然支持水平扩展
- 细粒度权限控制:RBAC 适合角色固定的企业系统,ABAC 适合动态策略的云平台
- 移动端 API:JWT 更适合无 Cookie 的移动端场景,Access Token + Refresh Token 保证安全与体验
对比认证方案:
| 维度 | Session/Cookie | JWT | OAuth 2.0 |
|---|---|---|---|
| 状态 | 有状态(服务端存储) | 无状态(令牌自包含) | 依赖授权服务器 |
| 扩展性 | 差(需共享 Session) | 好(各节点独立验证) | 好(集中授权) |
| 适用场景 | 单体应用/传统 Web | 前后端分离/微服务/API | 第三方登录/开放平台 |
| 安全性 | CSRF 风险/Cookie 劫持 | Token 泄露/XSS 风险 | 授权码泄露/重定向攻击 |
优缺点:
- ✅ Session/Cookie 优点:
- 实现简单,浏览器自动管理 Cookie
- 服务端可主动注销 Session
- Session ID 不暴露用户信息
- 同域请求天然携带,无需额外处理
- ❌ Session/Cookie 缺点:
- 分布式环境需共享 Session(Redis 等)
- 存在 CSRF 攻击风险
- 跨域配置复杂(CORS + Cookie)
- 移动端原生不支持 Cookie
- ✅ JWT 优点:
- 无状态,服务端无需存储,天然支持分布式
- 跨语言支持,任何能验证签名的服务都能认证
- 自包含用户信息,减少数据库查询
- 适合移动端和 API 场景
- ❌ JWT 缺点:
- 无法主动让单个 Token 失效(除非引入黑名单)
- Payload 默认仅 Base64 编码,不含敏感信息
- Token 体积比 Session ID 大
- 续期机制复杂,需要 Refresh Token 配合
- ✅ OAuth 2.0 优点:
- 行业标准协议,生态完善
- 用户无需在第三方应用输入密码
- 支持细粒度权限范围(Scope)
- 多种授权模式适配不同场景
- ❌ OAuth 2.0 缺点:
- 协议复杂,实现门槛高
- 依赖第三方授权服务可用性
- Token 管理和刷新逻辑复杂
- 安全陷阱多(重定向校验、State 参数等)
How — 怎么用
快速上手
# 安装依赖
npm install express jsonwebtoken express-jwt bcryptjs cookie-parser cors
npm install passport passport-local passport-github2
npm install express-session connect-redis # Session 方案
代码示例
1. Express + JWT 完整认证流程:
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
// 配置
const ACCESS_SECRET = 'your-access-secret-key';
const REFRESH_SECRET = 'your-refresh-secret-key';
const ACCESS_EXPIRES = '15m'; // 访问令牌短期
const REFRESH_EXPIRES = '7d'; // 刷新令牌长期
// 模拟用户数据库
const users = [];
// 注册
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// 检查用户是否已存在
const exists = users.find(u => u.username === username);
if (exists) return res.status(409).json({ error: '用户已存在' });
// 密码哈希
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: Date.now().toString(),
username,
password: hashedPassword,
role: 'user' // 默认角色
};
users.push(user);
res.status(201).json({ message: '注册成功', userId: user.id });
});
// 登录 — 签发双令牌
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) return res.status(401).json({ error: '用户名或密码错误' });
// 验证密码
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(401).json({ error: '用户名或密码错误' });
// 生成访问令牌
const accessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
ACCESS_SECRET,
{ expiresIn: ACCESS_EXPIRES }
);
// 生成刷新令牌
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: REFRESH_EXPIRES }
);
// 刷新令牌通过 httpOnly Cookie 传递,更安全
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // 防止 XSS 读取
secure: true, // 仅 HTTPS
sameSite: 'strict', // 防止 CSRF
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
});
res.json({ accessToken, user: { id: user.id, username: user.username, role: user.role } });
});
// 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({ error: '未提供访问令牌' });
jwt.verify(token, ACCESS_SECRET, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: '令牌无效' });
}
req.user = decoded;
next();
});
}
// 刷新令牌端点
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: '未提供刷新令牌' });
jwt.verify(refreshToken, REFRESH_SECRET, (err, decoded) => {
if (err) return res.status(403).json({ error: '刷新令牌无效' });
// 检查令牌类型
if (decoded.type !== 'refresh') {
return res.status(403).json({ error: '令牌类型错误' });
}
// 签发新的访问令牌
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
ACCESS_SECRET,
{ expiresIn: ACCESS_EXPIRES }
);
res.json({ accessToken: newAccessToken });
});
});
// 受保护路由
app.get('/profile', authenticateToken, (req, res) => {
res.json({ message: '这是受保护的资源', user: req.user });
});
// 登出 — 清除刷新令牌
app.post('/logout', (req, res) => {
res.clearCookie('refreshToken');
res.json({ message: '已登出' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
2. Passport.js OAuth2 第三方登录(GitHub):
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
const app = express();
// Session 配置(生产环境应使用 connect-redis 等存储)
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
}));
// Passport 初始化
app.use(passport.initialize());
app.use(passport.session());
// 序列化:将用户对象存入 Session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// 反序列化:从 Session 恢复用户对象
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// 配置 GitHub 策略
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/github/callback',
scope: ['user:email']
}, async (accessToken, refreshToken, profile, done) => {
try {
// 查找或创建用户
let user = await User.findOne({ githubId: profile.id });
if (!user) {
user = await User.create({
githubId: profile.id,
username: profile.username,
email: profile.emails?.[0]?.value,
avatar: profile.photos?.[0]?.value,
provider: 'github'
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}));
// 路由:发起 GitHub 授权
app.get('/auth/github', passport.authenticate('github'));
// 路由:GitHub 回调
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => {
// 授权成功,重定向到首页
res.redirect('/');
}
);
// 登出
app.get('/logout', (req, res, next) => {
req.logout((err) => {
if (err) return next(err);
res.redirect('/');
});
});
// 认证中间件
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/login');
}
// 受保护路由
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.json({ user: req.user });
});
app.listen(3000);
3. RBAC 权限中间件实现:
// roles.js — 角色与权限定义
const roles = {
admin: {
can: ['user:read', 'user:write', 'user:delete', 'post:read', 'post:write', 'post:delete', 'system:config']
},
editor: {
can: ['post:read', 'post:write', 'post:delete'],
inherits: ['viewer'] // 继承 viewer 的权限
},
viewer: {
can: ['post:read', 'user:read']
}
};
// 解析继承的权限
function getPermissions(role) {
const roleDef = roles[role];
if (!roleDef) return new Set();
const permissions = new Set(roleDef.can);
// 递归继承
if (roleDef.inherits) {
roleDef.inherits.forEach(parentRole => {
getPermissions(parentRole).forEach(p => permissions.add(p));
});
}
return permissions;
}
// RBAC 中间件工厂函数
function checkPermission(permission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '未认证' });
}
const userPermissions = getPermissions(req.user.role);
if (!userPermissions.has(permission)) {
return res.status(403).json({
error: '权限不足',
required: permission,
yourRole: req.user.role
});
}
next();
};
}
// 使用示例
const express = require('express');
const app = express();
// 路由级权限控制
app.get('/api/users', authenticateToken, checkPermission('user:read'), (req, res) => {
res.json({ users: [] });
});
app.post('/api/users', authenticateToken, checkPermission('user:write'), (req, res) => {
res.json({ message: '用户创建成功' });
});
app.delete('/api/users/:id', authenticateToken, checkPermission('user:delete'), (req, res) => {
res.json({ message: '用户删除成功' });
});
app.post('/api/posts', authenticateToken, checkPermission('post:write'), (req, res) => {
res.json({ message: '文章创建成功' });
});
// 资源级权限(ABAC 风格扩展)
function checkResourceAccess(action) {
return async (req, res, next) => {
const { userId, role } = req.user;
const resourceId = req.params.id;
// 管理员拥有所有权限
if (role === 'admin') return next();
// 检查是否是资源拥有者
const resource = await Resource.findById(resourceId);
if (!resource) return res.status(404).json({ error: '资源不存在' });
if (resource.ownerId === userId) return next();
// 检查角色权限
const permissions = getPermissions(role);
if (permissions.has(`${action}:own`) || permissions.has(`${action}:any`)) {
return next();
}
return res.status(403).json({ error: '无权访问此资源' });
};
}
4. 刷新令牌安全实现(含令牌轮换):
const crypto = require('crypto');
// 存储:生产环境应使用 Redis 或数据库
const refreshTokens = new Map(); // userId -> [{ token, expiresAt, family }]
// 生成安全的刷新令牌
function generateRefreshToken(userId) {
const token = crypto.randomBytes(64).toString('hex');
const family = crypto.randomBytes(32).toString('hex'); // 令牌族标识
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天
const tokens = refreshTokens.get(userId) || [];
tokens.push({ token, expiresAt, family });
refreshTokens.set(userId, tokens);
return { token, family };
}
// 刷新令牌端点 — 含令牌轮换和重放检测
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(401).json({ error: '未提供刷新令牌' });
// 查找令牌
let foundUser = null;
let foundTokenEntry = null;
for (const [userId, tokens] of refreshTokens) {
const entry = tokens.find(t => t.token === refreshToken);
if (entry) {
foundUser = userId;
foundTokenEntry = entry;
break;
}
}
if (!foundUser) {
// 令牌不存在 — 可能已被盗用
return res.status(403).json({ error: '刷新令牌无效' });
}
// 检查是否过期
if (foundTokenEntry.expiresAt < Date.now()) {
// 清除过期令牌
const tokens = refreshTokens.get(foundUser);
const filtered = tokens.filter(t => t.expiresAt >= Date.now());
refreshTokens.set(foundUser, filtered);
return res.status(401).json({ error: '刷新令牌已过期' });
}
// 令牌轮换:使旧令牌失效,签发新令牌
const tokens = refreshTokens.get(foundUser);
const oldIndex = tokens.indexOf(foundTokenEntry);
if (oldIndex === -1) {
// 令牌已被使用过 — 重放攻击!
// 安全措施:撤销该用户所有同族令牌
const family = foundTokenEntry.family;
const remaining = tokens.filter(t => t.family !== family);
refreshTokens.set(foundUser, remaining);
return res.status(403).json({ error: '检测到令牌重放,已撤销所有相关令牌' });
}
// 移除旧令牌
tokens.splice(oldIndex, 1);
// 签发新令牌(同族)
const newRefresh = generateRefreshTokenInFamily(foundUser, foundTokenEntry.family);
// 签发新的访问令牌
const newAccessToken = jwt.sign(
{ userId: foundUser },
ACCESS_SECRET,
{ expiresIn: ACCESS_EXPIRES }
);
res.json({
accessToken: newAccessToken,
refreshToken: newRefresh.token
});
});
function generateRefreshTokenInFamily(userId, family) {
const token = crypto.randomBytes(64).toString('hex');
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000;
const tokens = refreshTokens.get(userId) || [];
const entry = { token, expiresAt, family };
tokens.push(entry);
refreshTokens.set(userId, tokens);
return entry;
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| JWT 无法主动失效 | JWT 无状态,签发后无法撤销 | 引入 Token 黑名单(Redis 存储已注销 Token)或缩短有效期 + Refresh Token |
| CORS 携带 Cookie 失败 | 前端未设置 credentials: include 或后端 Allow-Origin 为 * | 前端设置 credentials: 'include',后端指定具体 Origin + Allow-Credentials: true |
| CSRF 攻击 | Cookie 自动携带导致跨站伪造请求 | 使用 SameSite Cookie + CSRF Token 双重防护,或改用 JWT(Authorization 头) |
| 刷新令牌被盗用 | Refresh Token 存储在 localStorage 易被 XSS 读取 | 将 Refresh Token 存在 httpOnly Cookie 中,Access Token 存内存 |
| JWT Payload 泄露敏感信息 | Payload 仅 Base64 编码,非加密 | 不在 Payload 中存放密码等敏感数据,只放 userId/role 等必要信息 |
| 多设备登录冲突 | 同一用户多端登录,踢出逻辑复杂 | 按设备维度管理 Token,踢出时删除对应设备的刷新令牌 |
| Passport.js 序列化性能差 | 每次 Session 请求都执行 deserializeUser | 减少 Session 存储的数据量,只存 userId,按需查询完整用户 |
| OAuth2 State 参数缺失 | 回调未校验 State,易受 CSRF 攻击 | 始终使用随机 State 参数,回调时严格校验一致性 |
最佳实践
- 令牌存储策略:Access Token 存内存(前端 JS 变量),Refresh Token 存 httpOnly Cookie,绝不存 localStorage
- HTTPS 强制:生产环境所有认证相关通信必须走 HTTPS,防止中间人攻击
- 令牌有效期设计:Access Token 短期(15-30 分钟),Refresh Token 长期(7-30 天),且实现令牌轮换
- 密码安全:使用 bcrypt(cost factor >= 10)哈希密码,永远不要存储明文密码或使用 MD5/SHA
- CSRF 双重防护:SameSite Cookie + CSRF Token 同时使用,API 认证优先使用 Authorization 头而非 Cookie
- JWT 签名算法:生产环境使用 RS256(非对称),私钥签发、公钥验证,微服务间无需共享密钥
- 权限最小化原则:OAuth Scope 只申请必要的最小权限,RBAC 角色只授予必要的最小权限集
- 审计与监控:记录所有登录/登出/权限变更事件,异常登录(异地、频繁失败)触发告警
- 会话管理:提供用户查看和撤销所有活跃会话的能力,修改密码后强制所有设备重新登录
- 输入验证:所有认证相关接口严格验证输入,防止 SQL 注入、XSS 等攻击
面试题
Q1: Session 认证和 JWT 认证的核心区别是什么?各适合什么场景?
核心区别在于状态管理:Session 是有状态的,服务端存储用户会话数据,客户端只持有 Session ID(通过 Cookie);JWT 是无状态的,用户信息编码在 Token 中,服务端不存储会话,通过验证签名确认合法性。Session 适合传统单体应用、同域 Web 应用,服务端可以主动管理会话生命周期(注销、踢人);JWT 适合前后端分离、微服务架构、移动端 API,无需共享 Session 存储,天然支持水平扩展。但 JWT 无法主动让单个 Token 失效,需要黑名单机制弥补。
Q2: JWT 的结构是什么?签名算法 HS256 和 RS256 有什么区别?生产环境推荐哪个?
JWT 由三部分组成:Header(指定算法和类型,Base64URL 编码)、Payload(存放声明数据如 userId/role/exp,Base64URL 编码)、Signature(对 Header.Payload 用密钥签名)。HS256 是对称算法,同一密钥既签名又验证,简单但密钥泄露风险高,微服务间需共享密钥。RS256 是非对称算法,私钥签名、公钥验证,各服务只需公钥即可验证,无需共享私钥。生产环境强烈推荐 RS256,尤其在微服务架构中,私钥只在认证服务持有,其他服务用公钥验证,安全性更高。
Q3: JWT 有哪些安全风险?如何防范?
主要安全风险:(1) Token 泄露 — XSS 攻击窃取 localStorage 中的 Token,防范:将 Token 存在内存中而非 localStorage,使用 httpOnly Cookie 传递 Refresh Token;(2) Token 无法撤销 — 签发后一直有效直到过期,防范:缩短 Access Token 有效期 + 黑名单机制;(3) 算法攻击 — 修改 Header 中的 alg 为 none 绕过验证,防范:服务端强制指定算法,不接受 Token 中的 alg 声明;(4) Payload 信息泄露 — Base64 不是加密,防范:不存放敏感数据,必要时使用 JWE 加密;(5) 重放攻击 — 截获 Token 后重放请求,防范:加入 jti(唯一标识)+ 时间窗口校验。
Q4: 请描述 OAuth 2.0 授权码模式的完整流程,为什么它比隐式模式更安全?
授权码模式流程:(1) 客户端将用户重定向到授权服务器(带 client_id、redirect_uri、scope、state 参数);(2) 用户在授权服务器登录并同意授权;(3) 授权服务器将用户重定向回客户端(redirect_uri),URL 中携带授权码 code 和 state;(4) 客户端后端用 code + client_secret 向授权服务器换取 Access Token;(5) 授权服务器返回 Token。比隐式模式更安全的原因:Token 不经过浏览器 URL(隐式模式 Token 在 URL fragment 中),而是通过后端到后端的安全通道获取;授权码是一次性的且有效期极短(通常 10 分钟);client_secret 只在后端使用,不暴露给前端。隐式模式已不推荐使用(OAuth 2.1 移除)。
Q5: Passport.js 的策略机制是如何工作的?如何自定义策略?
Passport.js 基于策略模式设计,每种认证方式是一个 Strategy 类。工作流程:(1) 通过
passport.use(new SomeStrategy(config, verify))注册策略,config 是配置参数,verify 是验证回调函数;(2) 调用passport.authenticate('strategy-name')作为路由中间件触发认证;(3) verify 回调接收认证结果(profile、token 等),查找或创建用户,调用done(null, user)或done(null, false)返回结果;(4) 序列化/反序列化通过serializeUser/deserializeUser管理 Session 中的用户标识。自定义策略只需继承passport-strategy,实现authenticate(req, options)方法,在其中完成验证逻辑并调用this.success(user)或this.fail(challenge)即可。
Q6: RBAC 和 ABAC 的区别是什么?分别在什么场景下使用?
RBAC(基于角色的访问控制)将权限绑定到角色,用户关联角色从而获得权限,如 admin 拥有所有权限、editor 可编辑文章、viewer 只能查看。优点是模型简单、易管理、审计方便;缺点是角色爆炸问题(权限组合多时角色数量激增)和缺乏上下文感知。ABAC(基于属性的访问控制)根据用户属性(部门/职级)、资源属性(密级/所有者)、环境属性(时间/IP)动态决策,如”工作时间+内网+本部门文档=可编辑”。优点是细粒度、灵活、支持动态策略;缺点是策略复杂、性能开销大、调试困难。RBAC 适合角色固定、权限稳定的企业系统(OA、ERP);ABAC 适合动态策略、细粒度控制的云平台(AWS IAM、多租户 SaaS)。
Q7: 刷新令牌的设计有哪些要点?什么是令牌轮换?
设计要点:(1) 双令牌架构 — Access Token 短期(15-30min),Refresh Token 长期(7-30d);(2) 存储分离 — Access Token 存前端内存,Refresh Token 存 httpOnly Cookie;(3) 令牌轮换 — 每次使用 Refresh Token 换取新 Access Token 时,同时签发新 Refresh Token,旧 Refresh Token 立即失效;(4) 重放检测 — 如果已失效的 Refresh Token 被再次使用,说明可能被盗用,应撤销该令牌族(同一签发链上的所有 Refresh Token);(5) 令牌族(Family) — 同一登录会话产生的 Refresh Token 属于同一族,便于批量撤销;(6) 安全传输 — Refresh Token 只通过 httpOnly + Secure + SameSite Cookie 传递。令牌轮换的核心目的是限制 Refresh Token 被盗后的窗口期:即使攻击者截获了旧的 Refresh Token,它已在使用后失效,无法再次使用。
Q8: CSRF 防护和认证方式有什么关系?为什么说 JWT(Authorization 头)天然防 CSRF?
CSRF(跨站请求伪造)利用浏览器自动携带 Cookie 的特性:恶意网站向目标网站发请求时,浏览器会自动附上用户的 Cookie,服务端无法区分请求来源。因此基于 Cookie 的 Session 认证天然受 CSRF 威胁。防护方式:(1) CSRF Token — 服务端生成随机 Token 嵌入表单/响应头,提交时校验一致性;(2) SameSite Cookie — 设置 SameSite=Strict/Lax 阻止跨站携带;(3) 检查 Referer/Origin 头。JWT 如果通过 Authorization 头传递(Bearer Token),浏览器不会自动附加此头,恶意网站的跨站请求无法携带 Token,因此天然免疫 CSRF。但 JWT 若存在 Cookie 中则仍有 CSRF 风险,需要配合 SameSite 或 CSRF Token 使用。
相关链接:
- Express
- 加密与数据安全
- Web安全防护
- Passport.js 官方文档:http://www.passportjs.org/