跳到主要内容

认证鉴权体系

问题

前端和服务端的认证鉴权方案有哪些?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 风险。
  • 双 TokenAccessToken(15min 短效)+ RefreshToken(7d 长效),401 时自动刷新并重放请求。
  • 退出失效:Redis 黑名单、用户表 tokenVersion 自增、或者干脆短有效期自然过期。
  • PKCE:SPA/移动端 OAuth 必加,用 code_verifier + code_challenge 防授权码截获。
  • 不要塞敏感信息:JWT payload 只是 Base64 编码不是加密,密码、身份证号绝对不能放。

答案

认证方案全景

方案对比

维度Session-CookieJWTOAuth 2.0SSO
状态有状态(服务端存储)无状态(客户端存储)基于 Token基于 Token
存储服务端内存/Redis客户端客户端认证中心
扩展性需要共享 Session天然支持分布式天然支持天然支持
安全性CSRF 风险XSS 风险较安全较安全
适用场景传统 Web 应用SPA、移动端、API第三方登录多系统统一登录
session-example.ts
// 服务端(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

jwt-example.ts
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 注意事项
  • JWT 无法主动失效:一旦签发,在过期前一直有效。需要黑名单机制或短有效期 + RefreshToken
  • JWT 体积比 Session ID 大
  • 不要在 JWT 中存敏感信息(payload 只是 Base64 编码,不是加密)

OAuth 2.0 授权码流程


常见面试问题

答案

存储位置优点缺点
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 步)

  1. 登录时服务端下发双 Token:用户登录成功后,服务端同时返回 accessToken(有效期 15min)和 refreshToken(有效期 7d)。前端把 accessToken 存在内存或 localStorage,refreshToken 存在 httpOnly Cookie 或单独的安全存储中(更安全)。

  2. 正常请求携带 AccessToken:每个 API 请求在 Authorization: Bearer <accessToken> 头中携带 AccessToken,服务端验证签名和有效期。

  3. AccessToken 过期,服务端返回 401:当 AccessToken 过期时,服务端返回 HTTP 401 Unauthorized。

  4. 前端拦截 401,发起刷新请求:Axios 响应拦截器捕获 401 后,自动调用 POST /auth/refresh 接口,携带 RefreshToken。服务端验证 RefreshToken 有效后,签发一对新的 AccessToken + RefreshToken(Refresh Token Rotation,旧 RefreshToken 立即作废,防止被盗用后无限续签)。

  5. 用新 Token 重放失败的请求:拿到新 AccessToken 后,替换掉原请求的 Authorization 头,自动重新发送之前因 401 失败的请求,用户完全无感知。

  6. 并发请求排队处理:如果 AccessToken 过期瞬间有多个请求同时 401,需要保证只发一次刷新请求。做法是用一个 isRefreshing 标志位 + 一个等待队列:第一个 401 触发刷新,后续 401 请求不再重复刷新,而是把自己的重试回调 push 进队列,等刷新成功后统一用新 Token 重放。

关键细节

  • RefreshToken 也过期了怎么办:刷新接口返回 401 或 403 → 清除所有 Token → 跳转登录页(此时用户需要重新登录)。
  • Refresh Token Rotation:每次刷新都颁发新的 RefreshToken 并废弃旧的。如果服务端检测到一个已作废的 RefreshToken 被使用(说明被盗了),应该立即吊销该用户所有 Token
  • 竞态安全:刷新期间的并发请求必须排队,否则会出现多次刷新导致 Token 版本冲突。
Axios 拦截器核心实现
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 本身无法撤销,常用方案:

  1. Redis 黑名单:退出时将 token 加入黑名单,每次请求检查
  2. 版本号:用户表加 tokenVersion,退出时 +1,JWT 中携带版本号比对
  3. 短有效期:AccessToken 5-15 分钟,过期自然失效

Q4: Session 和 JWT 怎么选?

答案

  • Session:传统 SSR 应用、需要即时踢人、安全性要求高
  • JWT:SPA、移动端、微服务、需要跨域/跨服务认证

Q5: PKCE 是什么?为什么需要?

答案

PKCE(Proof Key for Code Exchange)解决公共客户端(SPA、移动端)OAuth 授权码被截获的风险:

  1. 客户端生成随机 code_verifier,计算 code_challenge = SHA256(code_verifier)
  2. 授权请求携带 code_challenge
  3. 换 Token 时携带 code_verifier,服务端验证匹配

即使授权码被截获,没有 code_verifier 也无法换取 Token。

相关链接