阶段二:RESTful进阶
认证(Authentication)回答"你是谁",授权(Authorization)回答"你能做什么"。这两个概念是API安全的基石。本课从Session到JWT,从API Key到OAuth2,深入理解主流认证授权方案的原理、实现和选型。
| 方案 | 状态 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Session+Cookie | 有状态 | 传统Web应用 | 简单成熟 | 不支持跨域,难扩展 |
| JWT | 无状态 | API/微服务 | 无状态、可扩展 | 令牌较大、难撤销 |
| API Key | 无状态 | 服务间调用 | 简单轻量 | 权限粒度粗 |
| OAuth2 | 可变 | 第三方授权 | 标准化、安全 | 实现复杂 |
JWT(JSON Web Token)由三部分组成:Header.Payload.Signature,用.分隔。
// JWT结构
// Header(算法和类型)
const header = { alg: 'HS256', typ: 'JWT' };
// Payload(声明/claims)
const payload = {
sub: '42', // Subject - 用户ID
name: '张三', // 自定义声明
role: 'admin', // 自定义声明
iat: 1705312800, // Issued At - 签发时间
exp: 1705399200, // Expiration - 过期时间
iss: 'api.example.com', // Issuer - 签发者
aud: 'app.example.com', // Audience - 接收者
jti: 'abc123', // JWT ID - 唯一标识(防重放)
};
// Signature(签名)
// HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
// 确保令牌未被篡改
// 编码后的JWT
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiLlvKDkuYoiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDUzMTI4MDAsImV4cCI6MTcwNTM5OTIwMH0.signature
// auth-jwt.js - JWT认证完整实现
const crypto = require('crypto');
// ===== JWT工具 =====
function base64UrlEncode(data) {
const str = typeof data === 'string' ? data : JSON.stringify(data);
return Buffer.from(str)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function base64UrlDecode(str) {
const padded = str + '='.repeat((4 - str.length % 4) % 4);
return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString();
}
function sign(payload, secret, algorithm = 'HS256') {
const header = { alg: algorithm, typ: 'JWT' };
const headerEncoded = base64UrlEncode(header);
const payloadEncoded = base64UrlEncode(payload);
const signatureInput = `${headerEncoded}.${payloadEncoded}`;
const signature = crypto.createHmac('sha256', secret)
.update(signatureInput)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return `${headerEncoded}.${payloadEncoded}.${signature}`;
}
function verify(token, secret) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('无效的JWT格式');
const [headerEncoded, payloadEncoded, signature] = parts;
// 验证签名
const expectedSignature = crypto.createHmac('sha256', secret)
.update(`${headerEncoded}.${payloadEncoded}`)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
if (signature !== expectedSignature) {
throw new Error('签名验证失败');
}
// 解析payload
const payload = JSON.parse(base64UrlDecode(payloadEncoded));
// 检查过期
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('令牌已过期');
}
// 检查生效时间
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) {
throw new Error('令牌尚未生效');
}
return payload;
}
// ===== Token管理 =====
const SECRET = process.env.JWT_SECRET || 'your-256-bit-secret-key-here';
const ACCESS_TOKEN_EXPIRY = '15m'; // 访问令牌15分钟
const REFRESH_TOKEN_EXPIRY = '7d'; // 刷新令牌7天
function generateAccessToken(user) {
const now = Math.floor(Date.now() / 1000);
return sign({
sub: String(user.id),
username: user.username,
role: user.role,
type: 'access',
iat: now,
exp: now + 15 * 60, // 15分钟
jti: crypto.randomUUID(),
}, SECRET);
}
function generateRefreshToken(user) {
const now = Math.floor(Date.now() / 1000);
return sign({
sub: String(user.id),
type: 'refresh',
iat: now,
exp: now + 7 * 24 * 60 * 60, // 7天
jti: crypto.randomUUID(),
}, SECRET);
}
// ===== Token黑名单(简化版,生产用Redis) =====
const tokenBlacklist = new Set();
function revokeToken(jti) {
tokenBlacklist.add(jti);
}
function isTokenRevoked(jti) {
return tokenBlacklist.has(jti);
}
// ===== 认证中间件 =====
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_MISSING', message: '缺少认证令牌' },
});
}
const token = authHeader.substring(7);
try {
const payload = verify(token, SECRET);
// 检查令牌类型
if (payload.type !== 'access') {
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_TOKEN', message: '无效的访问令牌' },
});
}
// 检查黑名单
if (isTokenRevoked(payload.jti)) {
return res.status(401).json({
success: false,
error: { code: 'TOKEN_REVOKED', message: '令牌已被撤销' },
});
}
req.user = payload;
next();
} catch (err) {
const message = err.message.includes('过期') ? '令牌已过期' : '令牌无效';
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_TOKEN', message },
});
}
}
// 授权中间件
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_MISSING', message: '未认证' },
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: `需要角色: ${roles.join('/')}` },
});
}
next();
};
}
// ===== Express应用 =====
const express = require('express');
const app = express();
app.use(express.json());
// 模拟用户数据库
const users = new Map();
users.set(1, { id: 1, username: 'admin', password: hashPassword('admin123'), role: 'admin' });
users.set(2, { id: 2, username: 'editor', password: hashPassword('editor123'), role: 'editor' });
function hashPassword(pwd) {
return crypto.createHash('sha256').update(pwd + 'salt').digest('hex');
}
// 登录
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
const user = [...users.values()].find(u => u.username === username);
if (!user || user.password !== hashPassword(password)) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_CREDENTIAL', message: '用户名或密码错误' },
});
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.json({
success: true,
data: {
accessToken,
refreshToken,
expiresIn: 900, // 15分钟 = 900秒
tokenType: 'Bearer',
},
});
});
// 刷新令牌
app.post('/api/auth/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_MISSING', message: '缺少刷新令牌' },
});
}
try {
const payload = verify(refreshToken, SECRET);
if (payload.type !== 'refresh') {
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_TOKEN', message: '无效的刷新令牌' },
});
}
const user = users.get(parseInt(payload.sub));
if (!user) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_TOKEN', message: '用户不存在' },
});
}
// 撤销旧刷新令牌
revokeToken(payload.jti);
// 生成新令牌对
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
res.json({
success: true,
data: {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: 900,
},
});
} catch (err) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_INVALID_TOKEN', message: '刷新令牌无效或已过期' },
});
}
});
// 登出
app.post('/api/auth/logout', authenticate, (req, res) => {
revokeToken(req.user.jti);
res.json({ success: true, data: { message: '已登出' } });
});
// 受保护的资源 - 任何认证用户
app.get('/api/profile', authenticate, (req, res) => {
const user = users.get(parseInt(req.user.sub));
res.json({ success: true, data: { id: user.id, username: user.username, role: user.role } });
});
// 受保护的资源 - 仅管理员
app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
res.json({ success: true, data: { message: '管理员面板', totalUsers: users.size } });
});
app.listen(3000, () => console.log('🔐 JWT认证服务运行在 http://localhost:3000'));
# 1. 登录获取令牌
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' | jq -r '.data.accessToken')
echo "Token: $TOKEN"
# 2. 用令牌访问受保护资源
curl -s http://localhost:3000/api/profile \
-H "Authorization: Bearer $TOKEN" | jq .
{
"success": true,
"data": {"id":1,"username":"admin","role":"admin"}
}
# 3. 无令牌访问 - 401
curl -s http://localhost:3000/api/profile | jq .
# 4. 管理员访问管理面板
curl -s http://localhost:3000/api/admin/dashboard \
-H "Authorization: Bearer $TOKEN" | jq .
# 5. 普通用户尝试访问 - 403
EDITOR_TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"editor","password":"editor123"}' | jq -r '.data.accessToken')
curl -s http://localhost:3000/api/admin/dashboard \
-H "Authorization: Bearer $EDITOR_TOKEN" | jq .
# 步骤1: 重定向到授权端点
GET /oauth/authorize?
response_type=code&
client_id=myapp&
redirect_uri=https://myapp.com/callback&
scope=read+write&
state=random_csrf_token
# 步骤3: 回调收到授权码
GET /callback?
code=AUTHORIZATION_CODE&
state=random_csrf_token
# 步骤4: 用授权码换令牌
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://myapp.com/callback&
client_id=myapp&
client_secret=SECRET
| 模式 | 适用场景 | 安全性 |
|---|---|---|
| 隐式模式(Implicit) | 纯前端SPA(已不推荐) | ⚠️ 低,令牌暴露在URL |
| 密码模式(Resource Owner Password) | 高度信任的第一方应用 | ⚠️ 中,需用户直接提供密码 |
| 客户端凭证(Client Credentials) | 服务间调用(无用户参与) | ✅ 高,无需用户交互 |
// api-key-auth.js - API Key认证
const crypto = require('crypto');
// 生成API Key
function generateApiKey(prefix = 'sk') {
const key = crypto.randomBytes(24).toString('hex');
return `${prefix}_${key}`;
}
// API Key存储(生产环境用数据库)
const apiKeys = new Map();
// 注册API Key
function registerApiKey(userId, permissions = []) {
const key = generateApiKey();
const hashed = crypto.createHash('sha256').update(key).digest('hex');
apiKeys.set(hashed, {
userId,
permissions,
createdAt: new Date().toISOString(),
lastUsed: null,
requestCount: 0,
});
return key; // 只在创建时返回明文
}
// API Key中间件
function apiKeyAuth(req, res, next) {
const key = req.headers['x-api-key'] || req.query.api_key;
if (!key) {
return res.status(401).json({
success: false,
error: { code: 'API_KEY_MISSING', message: '缺少API Key' },
});
}
const hashed = crypto.createHash('sha256').update(key).digest('hex');
const keyData = apiKeys.get(hashed);
if (!keyData) {
return res.status(401).json({
success: false,
error: { code: 'API_KEY_INVALID', message: '无效的API Key' },
});
}
// 更新使用统计
keyData.lastUsed = new Date().toISOString();
keyData.requestCount++;
req.apiKey = keyData;
next();
}
设计一个权限系统,包含admin/editor/viewer三种角色,实现对不同资源的不同操作权限。
用Express实现一个简化版的OAuth2授权服务器,包含/authorize和/token两个端点。