认证鉴权体系
问题
前端和服务端的认证鉴权方案有哪些?Session、JWT、OAuth、SSO 各有什么特点?
Session、JWT、OAuth、SSO 各自适合什么场景? 四种方案核心区别在「状态保存在哪」和「为谁解决问题」:
- Session-Cookie:有状态,会话存服务端 Redis,客户端只拿 SessionID;适合传统 SSR、需要即时踢人。
- JWT:无状态,用户信息编码在 Token 里,天然支持分布式;适合 SPA、移动端、微服务。
- OAuth 2.0:第三方登录协议(GitHub/微信登录),核心流程是授权码换 Token。
- SSO:多系统统一认证中心,一次登录全站通行,常基于 CAS 或 OIDC 实现。
JWT 实战要注意什么?怎么实现无感刷新和退出登录? JWT 最大的坑是「无法主动失效」,需要用机制弥补:
- 存储位置:
httpOnly Cookie + SameSite=Lax最安全(防 XSS),localStorage 方便但有 XSS 风险。 - 双 Token:
AccessToken(15min 短效)+RefreshToken(7d 长效),401 时自动刷新并重放请求。 - 退出失效:Redis 黑名单、用户表
tokenVersion自增、或者干脆短有效期自然过期。 - PKCE:SPA/移动端 OAuth 必加,用
code_verifier+code_challenge防授权码截获。 - 不要塞敏感信息:JWT payload 只是 Base64 编码不是加密,密码、身份证号绝对不能放。
答案
认证方案全景
方案对比
| 维度 | Session-Cookie | JWT | OAuth 2.0 | SSO |
|---|---|---|---|---|
| 状态 | 有状态(服务端存储) | 无状态(客户端存储) | 基于 Token | 基于 Token |
| 存储 | 服务端内存/Redis | 客户端 | 客户端 | 认证中心 |
| 扩展性 | 需要共享 Session | 天然支持分布式 | 天然支持 | 天然支持 |
| 安全性 | CSRF 风险 | XSS 风险 | 较安全 | 较安全 |
| 适用场景 | 传统 Web 应用 | SPA、移动端、API | 第三方登录 | 多系统统一登录 |
Session-Cookie
// 服务端(Express + express-session)
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // 防 XSS
secure: true, // 仅 HTTPS
sameSite: 'lax', // 防 CSRF
maxAge: 24 * 60 * 60 * 1000, // 1 天
},
}));
// 登录
app.post('/login', (req, res) => {
// 验证用户名密码...
req.session.userId = user.id;
res.json({ success: true });
});
JWT
import jwt from 'jsonwebtoken';
// 签发 Token
const generateTokens = (userId: string) => {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
};
// 验证中间件
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch {
return res.status(401).json({ error: 'Token expired' });
}
};
- JWT 无法主动失效:一旦签发,在过期前一直有效。需要黑名单机制或短有效期 + RefreshToken
- JWT 体积比 Session ID 大
- 不要在 JWT 中存敏感信息(payload 只是 Base64 编码,不是加密)
OAuth 2.0 授权码流程
常见面试问题
Q1: JWT 存在 localStorage 还是 Cookie 中?
答案:
| 存储位置 | 优点 | 缺点 |
|---|---|---|
| localStorage | 不会自动发送,无 CSRF 风险 | XSS 可窃取 |
| Cookie (httpOnly) | XSS 无法读取 | 需防 CSRF |
推荐:httpOnly Cookie + SameSite=Lax,安全性最高。如果是纯 SPA + 跨域 API,用 localStorage + 短有效期也可接受。
Q2: Token 无感刷新怎么做?
答案:
整体思路:双 Token 机制——AccessToken(短期,通常 15min)负责鉴权,RefreshToken(长期,通常 7-14d)负责续签。AccessToken 过期后自动用 RefreshToken 换新的,用户全程无感知。
完整流程(分 6 步):
-
登录时服务端下发双 Token:用户登录成功后,服务端同时返回
accessToken(有效期 15min)和refreshToken(有效期 7d)。前端把accessToken存在内存或 localStorage,refreshToken存在 httpOnly Cookie 或单独的安全存储中(更安全)。 -
正常请求携带 AccessToken:每个 API 请求在
Authorization: Bearer <accessToken>头中携带 AccessToken,服务端验证签名和有效期。 -
AccessToken 过期,服务端返回 401:当 AccessToken 过期时,服务端返回 HTTP 401 Unauthorized。
-
前端拦截 401,发起刷新请求:Axios 响应拦截器捕获 401 后,自动调用
POST /auth/refresh接口,携带 RefreshToken。服务端验证 RefreshToken 有效后,签发一对新的 AccessToken + RefreshToken(Refresh Token Rotation,旧 RefreshToken 立即作废,防止被盗用后无限续签)。 -
用新 Token 重放失败的请求:拿到新 AccessToken 后,替换掉原请求的 Authorization 头,自动重新发送之前因 401 失败的请求,用户完全无感知。
-
并发请求排队处理:如果 AccessToken 过期瞬间有多个请求同时 401,需要保证只发一次刷新请求。做法是用一个
isRefreshing标志位 + 一个等待队列:第一个 401 触发刷新,后续 401 请求不再重复刷新,而是把自己的重试回调 push 进队列,等刷新成功后统一用新 Token 重放。
关键细节:
- RefreshToken 也过期了怎么办:刷新接口返回 401 或 403 → 清除所有 Token → 跳转登录页(此时用户需要重新登录)。
- Refresh Token Rotation:每次刷新都颁发新的 RefreshToken 并废弃旧的。如果服务端检测到一个已作废的 RefreshToken 被使用(说明被盗了),应该立即吊销该用户所有 Token。
- 竞态安全:刷新期间的并发请求必须排队,否则会出现多次刷新导致 Token 版本冲突。
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: unknown) => void;
}> = [];
// 处理排队的请求:刷新成功后用新 token 统一重放
function processQueue(error: unknown, token: string | null) {
failedQueue.forEach(({ resolve, reject }) => {
error ? reject(error) : resolve(token!);
});
failedQueue = [];
}
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 不是 401,或者是刷新接口本身失败 → 直接抛出
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
// 如果正在刷新中,把当前请求挂起排队
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(axios(originalRequest));
},
reject,
});
});
}
// 第一个 401:发起刷新
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axios.post('/auth/refresh', {
refreshToken: getRefreshToken(),
});
// 存储新 Token
setAccessToken(data.accessToken);
setRefreshToken(data.refreshToken);
// 用新 Token 重放当前请求
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
// 释放队列中等待的请求
processQueue(null, data.accessToken);
return axios(originalRequest);
} catch (refreshError) {
// RefreshToken 也失效 → 清除登录态,跳转登录
processQueue(refreshError, null);
logout();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
Q3: 如何实现退出登录让 JWT 失效?
答案:
JWT 本身无法撤销,常用方案:
- Redis 黑名单:退出时将 token 加入黑名单,每次请求检查
- 版本号:用户表加
tokenVersion,退出时 +1,JWT 中携带版本号比对 - 短有效期:AccessToken 5-15 分钟,过期自然失效
Q4: Session 和 JWT 怎么选?
答案:
- Session:传统 SSR 应用、需要即时踢人、安全性要求高
- JWT:SPA、移动端、微服务、需要跨域/跨服务认证
Q5: PKCE 是什么?为什么需要?
答案:
PKCE(Proof Key for Code Exchange)解决公共客户端(SPA、移动端)OAuth 授权码被截获的风险:
- 客户端生成随机
code_verifier,计算code_challenge = SHA256(code_verifier) - 授权请求携带
code_challenge - 换 Token 时携带
code_verifier,服务端验证匹配
即使授权码被截获,没有 code_verifier 也无法换取 Token。
相关链接
- JWT 认证 - JWT 结构与实现
- Cookie 与 Session - Cookie/Session 详解
- 设计单点登录系统 - SSO 系统设计
- Token 无感刷新 - Token 刷新实战