Skip to content
Session、JWT、OAuth2
概述
HTTP 是无状态协议,服务器默认无法区分两次请求是否来自同一个用户。认证与授权机制的作用是在无状态传输之上建立会话标识,让服务端识别用户身份并控制资源访问。常见实现方式有三种:基于服务端存储的 Session 模式、基于签名令牌的 JWT 模式,以及基于第三方授权的 OAuth2 模式。
Session 模式
基本概念
Session 模式将用户登录后的身份信息保存在服务端(内存、数据库或缓存),只向客户端下发一个不包含业务数据的唯一标识符(Session ID)。浏览器收到后将其存入 Cookie,后续请求自动携带,服务器根据该标识查找对应的会话记录。
工作原理
- 用户提交凭证,服务器验证通过后创建 Session 对象,生成唯一 ID。
- 服务器通过
Set-Cookie将 Session ID 发送给客户端。 - 客户端后续请求自动在 Cookie 头中带上该 ID。
- 服务器拦截请求,从存储中恢复 Session,将用户信息挂入请求上下文。
- 注销或超时时销毁对应 Session 记录。
基本用法
以 Express 和 express-session 为例:
javascript
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 3600000 }
}));
app.post('/login', (req, res) => {
// 验证用户名密码后
req.session.user = { id: 1, role: 'admin' };
res.send('ok');
});
app.get('/dashboard', (req, res) => {
if (!req.session.user) return res.sendStatus(401);
res.send(`Hello, ${req.session.user.role}`);
});当客户端访问 /dashboard 时,express-session 解析 Cookie 中的 Session ID,从存储中取出会话数据填充到 req.session,后续处理函数可以直接读取。
注意点
- 会话存储:默认使用内存存储,多进程或多服务器部署时需切换到 Redis、数据库等共享存储。
- 会话固定:登录成功后应调用
req.session.regenerate()重新生成 Session ID,防止会话固定攻击。 - 强制下线与并发控制:会话在服务端集中管理,能够直接删除某个 Session 实现强制下线,也可以根据业务逻辑限制同一账号的并发会话数。
JWT 模式
基本概念
JWT(JSON Web Token)是一种自包含的令牌格式,将用户身份声明(Claims)与签名打包在一起。服务端不保留会话,客户端每次请求提交令牌,服务器通过验证签名和有效期来确认身份。典型的 JWT 由 Header.Payload.Signature 三部分组成。
工作原理
- 用户登录成功后,服务器生成一个包含用户标识、过期时间的 JWT,使用密钥签名后返回。
- 客户端将令牌存储在
localStorage或内存中,并在请求的Authorization头中以Bearer <token>形式发送。 - 服务器解析令牌,验证签名和过期时间,提取声明后设置安全上下文。
- 令牌失效前无需再次查询存储;注销通常依靠客户端丢弃令牌,或服务端使用黑名单、短有效期加刷新令牌策略。
生成 JWT
使用 jsonwebtoken 库创建令牌,必须指定签名算法和过期时间:
javascript
const jwt = require('jsonwebtoken');
const payload = {
sub: '10001',
name: 'Alice',
role: 'user'
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '2h'
});生成的 token 即是完整的 JWT,例如: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...
若密钥泄露,任何人都可以伪造令牌,因此 JWT_SECRET 必须通过环境变量注入。
验证 JWT 并设置上下文
在 Express 中编写中间件对每个请求进行令牌验证:
javascript
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }, (err, decoded) => {
if (err) return res.sendStatus(403);
req.user = decoded;
next();
});
}将中间件挂载到需要保护的路由:
javascript
app.get('/api/orders', authenticateToken, (req, res) => {
// req.user 中包含了 sub, name, role 等声明
res.json({ orders: [], user: req.user.name });
});验证失败时返回 401(无令牌或格式错误)或 403(签名无效或令牌过期),不会将假用户信息填入上下文。
注意点
- 算法协商:验证时固定允许的算法列表,避免 “none” 算法绕过攻击。
- 有效期设计:访问令牌应设置较短有效期(如 15 分钟),配合刷新令牌轮换。
- 载荷体积:不要将大量数据放入 JWT,每次请求都会传输,会增加带宽消耗。
- 注销困境:JWT 发出后无法主动撤销,需要借助黑名单机制或缩短有效期降低风险。
- 安全存储:单页应用中令牌不应存储在
localStorage以免被 XSS 窃取,推荐httpOnlyCookie 搭配 BFF 模式。
刷新令牌轮换
当访问令牌(access token)有效期较短时,服务端同时下发一个刷新令牌(refresh token),用于在访问令牌过期后获取新的访问令牌,避免用户频繁登录。
登录接口同时返回两种令牌:
javascript
function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role, type: 'access' },
process.env.JWT_ACCESS_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ algorithm: 'HS256', expiresIn: '7d' }
);
return { accessToken, refreshToken };
}accessToken 和 refreshToken 使用不同的密钥,且 refreshToken 的有效期更长。刷新端点接收 refreshToken,验证后下发新的 accessToken(以及可选的轮换 refreshToken):
javascript
app.post('/auth/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.sendStatus(401);
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, { algorithms: ['HS256'] }, (err, decoded) => {
if (err || decoded.type !== 'refresh') return res.sendStatus(403);
// 实际环境中应检查此 refreshToken 是否已被撤销(如黑名单)
const user = { id: decoded.sub, role: 'user' };
const tokens = generateTokens(user);
res.json(tokens);
});
});验证刷新令牌时需额外检查 type 字段,防止将普通的访问令牌当作刷新令牌使用。为避免令牌泄露后长期有效,建议每次使用刷新令牌时废弃旧的刷新令牌并发放新的(轮换),同时将旧令牌加入撤销列表。
客户端在检测到访问令牌过期后,使用刷新令牌调用 /auth/refresh,用收到的新的访问令牌重试原请求。刷新令牌本身过期则需要用户重新登录。
OAuth2 登录
基本概念
OAuth2 是一种授权框架,允许用户让第三方应用访问自己在某一服务提供商上的资源,而无需将凭证直接交给第三方。常用于“使用 XX 账号登录”功能。主要角色包括资源拥有者、客户端、授权服务器、资源服务器。常用授权模式有授权码模式、隐式模式、客户端凭证模式等。
工作原理(授权码模式)
- 用户点击“使用 GitHub 登录”,浏览器重定向至授权服务器的授权端点,携带客户端标识、回调 URI 和 scope。
- 用户在授权服务器上登录并同意授权。
- 授权服务器通过浏览器重定向到回调 URI,并附带授权码。
- 客户端服务器用授权码、客户端 ID 和 Secret 向授权服务器的令牌端点换取访问令牌。
- 使用访问令牌请求资源服务器获取用户信息,完成登录流程。
基本用法
使用 passport 和 passport-github2 实现 OAuth2 登录:
javascript
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'https://yourapp.com/auth/github/callback'
}, (accessToken, refreshToken, profile, done) => {
// 查找或创建用户
return done(null, profile);
}));Express 中挂载路由:
javascript
app.get('/auth/github', passport.authenticate('github', { scope: ['user:email'] }));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => res.redirect('/dashboard')
);配置文件中的 clientID 和 clientSecret 绝不能硬编码,应从环境变量读取,避免密钥泄露。
注意点
- 回调 URI 校验:授权服务器会严格比对回调 URI,必须在应用注册时列入白名单。
- state 参数:在授权请求中携带随机生成的
state值,回调时比对,防范 CSRF。 - 令牌交换:服务器端完成授权码与令牌的交换,前端不应接触 Client Secret。
- HTTPS 强制:整个流程依赖 TLS 保护,禁用 HTTP 明文传输。
资源服务器 JWT
基本概念
当系统仅作为资源服务器,不负责颁发令牌时,只需配置 JWT 解码规则来验证传入令牌的签名和声明,通常依赖授权服务器公布的 JWKS(JSON Web Key Set)端点或共享密钥。
基本用法
javascript
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.publicKey || key?.rsaPublicKey;
callback(null, signingKey);
});
}
function verifyResourceToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) return res.sendStatus(403);
req.user = decoded;
next();
});
}此中间件只负责签名验证和声明提取,不关心颁发逻辑。
注意点
- 签发者校验:验证时应检查
iss字段是否与授权的发行方匹配。 - 密钥轮换:使用 JWKS 客户端自动拉取最新签名密钥。
- 性能:签名验证过程会异步获取公钥,可适度缓存。
选型
| 模式 | 适用场景 | 典型限制 |
|---|---|---|
| Session | 传统的服务端渲染页面、后台管理系统 | 依赖共享存储以支撑水平扩展 |
| JWT | 前后端分离的 API、网关背后多个服务 | 无法服务端主动注销(除非加入黑名单) |
| OAuth2 登录 | 第三方身份集成、统一认证平台 | 依赖外部授权服务器的可用性 |
| 资源服务器 JWT | 只消费令牌的内部服务 | 令牌由独立的授权服务器签发,需协同配置 |
