HTTP 协议详解教程 / 第 9 章:认证与授权
第 9 章:认证与授权
认证(Authentication)解决"你是谁"的问题,授权(Authorization)解决"你能做什么"的问题。本章全面介绍 HTTP 中的认证方案。
9.1 认证 vs 授权
| 概念 | 英文 | 回答的问题 | HTTP 头部 |
|---|
| 认证 | Authentication | 你是谁? | Authorization |
| 授权 | Authorization | 你能做什么? | Authorization + 权限检查 |
用户请求 → 认证(你是谁?) → 授权(你有权限吗?) → 处理请求
↓ 失败 ↓ 失败
401 Unauthorized 403 Forbidden
9.2 HTTP Basic 认证
原理
# 服务端返回 401,告知需要认证
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="My Application"
# 客户端发送凭据(Base64 编码的 username:password)
GET /api/data HTTP/1.1
Authorization: Basic dXNlcjpwYXNzd29yZA==
代码实现
import requests
from requests.auth import HTTPBasicAuth
# 方式 1:使用 HTTPBasicAuth
response = requests.get(
'https://api.example.com/data',
auth=HTTPBasicAuth('username', 'password')
)
# 方式 2:使用元组简写
response = requests.get(
'https://api.example.com/data',
auth=('username', 'password')
)
# 方式 3:手动构建 Header
import base64
credentials = base64.b64encode(b'username:password').decode()
response = requests.get(
'https://api.example.com/data',
headers={'Authorization': f'Basic {credentials}'}
)
# curl Basic 认证
curl -u username:password https://api.example.com/data
# 或者
curl -H "Authorization: Basic $(echo -n 'user:pass' | base64)" https://api.example.com/data
服务端实现
const express = require('express');
const app = express();
function basicAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="My Application"');
return res.status(401).json({ error: '请提供认证凭据' });
}
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
const [username, password] = credentials.split(':');
if (username === 'admin' && password === 'secret') {
req.user = { username };
next();
} else {
res.setHeader('WWW-Authenticate', 'Basic realm="My Application"');
return res.status(401).json({ error: '凭据无效' });
}
}
app.get('/api/admin', basicAuth, (req, res) => {
res.json({ message: `欢迎,${req.user.username}` });
});
优缺点
| 优点 | 缺点 |
|---|
| 简单通用 | 每次请求都发送密码 |
| 浏览器原生支持 | Base64 不是加密 |
| 无状态 | 凭据无法过期 |
⚠️ 安全警告:Basic 认证必须配合 HTTPS 使用,否则密码明文传输。
9.3 Bearer Token 认证
原理
GET /api/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
完整的 Token 认证流程
1. 登录获取 Token
POST /api/login
{"email": "[email protected]", "password": "P@ssw0rd"}
响应:
{
"access_token": "eyJhbGci...",
"refresh_token": "dGhpcyBp...",
"expires_in": 3600,
"token_type": "Bearer"
}
2. 使用 Token 访问 API
GET /api/users
Authorization: Bearer eyJhbGci...
3. Token 过期后刷新
POST /api/refresh
{"refresh_token": "dGhpcyBp..."}
服务端实现
const jwt = require('jsonwebtoken');
const ACCESS_SECRET = 'access-secret-key';
const REFRESH_SECRET = 'refresh-secret-key';
// 登录
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await findUser(email);
if (!user || !await verifyPassword(password, user.hash)) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
ACCESS_SECRET,
{ expiresIn: '1h' }
);
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
await storeRefreshToken(user.id, refreshToken);
res.json({
access_token: accessToken,
refresh_token: refreshToken,
expires_in: 3600,
token_type: 'Bearer'
});
});
// 刷新 Token
app.post('/api/refresh', async (req, res) => {
const { refresh_token } = req.body;
try {
const payload = jwt.verify(refresh_token, REFRESH_SECRET);
const stored = await getRefreshToken(payload.sub);
if (stored !== refresh_token) {
return res.status(401).json({ error: '无效的刷新令牌' });
}
const newAccessToken = jwt.sign(
{ sub: payload.sub },
ACCESS_SECRET,
{ expiresIn: '1h' }
);
res.json({
access_token: newAccessToken,
expires_in: 3600,
token_type: 'Bearer'
});
} catch (err) {
res.status(401).json({ error: '刷新令牌已过期,请重新登录' });
}
});
// 认证中间件
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '请提供认证令牌' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, ACCESS_SECRET);
req.user = payload;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: '无效的令牌' });
}
}
9.4 JWT(JSON Web Token)
JWT 结构
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpM
├──── Header ────┤├──── Payload ────┤├──── Signature ────────┤
| 部分 | 内容 | 编码 |
|---|
| Header | 算法、类型 | Base64URL |
| Payload | 声明(Claims) | Base64URL |
| Signature | 签名 | HMAC/RS256 |
Payload 常见字段
| 字段 | 含义 | 示例 |
|---|
sub | 主题(用户 ID) | "12345" |
iss | 签发者 | "api.example.com" |
aud | 受众 | "web.example.com" |
exp | 过期时间 | 1715328000 |
iat | 签发时间 | 1715324400 |
nbf | 生效时间 | 1715324400 |
jti | JWT ID | "unique-id" |
// Python JWT 示例
const jwt = require('jsonwebtoken');
// 签发 Token
const token = jwt.sign(
{
sub: '12345',
email: '[email protected]',
role: 'admin'
},
'secret-key',
{
algorithm: 'HS256',
expiresIn: '1h',
issuer: 'api.example.com'
}
);
console.log(token);
// eyJhbGciOiJIUzI1NiIs...
// 验证 Token
try {
const payload = jwt.verify(token, 'secret-key', {
algorithms: ['HS256'],
issuer: 'api.example.com'
});
console.log(payload);
// { sub: '12345', email: '[email protected]', role: 'admin', iat: ..., exp: ... }
} catch (err) {
console.error('Token 无效:', err.message);
}
JWT 安全注意事项
| 注意 | 说明 |
|---|
| 不要存敏感信息 | Payload 只是 Base64,不是加密 |
| 使用强密钥 | HS256 至少 256 位密钥 |
| 设置过期时间 | exp 必须设置 |
| 验证所有字段 | iss、aud、exp 都要验证 |
| 短生命周期 | Access Token 建议 15-60 分钟 |
9.5 OAuth 2.0
四种授权模式
| 模式 | 适用场景 | 安全性 |
|---|
| 授权码模式(Authorization Code) | Web 应用 | 最高 |
| 授权码 + PKCE | SPA、移动端 | 高 |
| 客户端凭据(Client Credentials) | 机器对机器 | 高 |
| 隐式模式(Implicit) | 已废弃 | 低 |
授权码流程
┌──────┐ ┌──────────┐ ┌──────────┐
│Client│ │Auth Server│ │ Resource │
└──┬───┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. 重定向到授权页 │ │
│───────────────────────→│ │
│ │ │
│ 2. 用户授权 │ │
│←──────────────────────│ │
│ │ │
│ 3. 用授权码换取 Token │ │
│───────────────────────→│ │
│ │ │
│ 4. 返回 Access Token │ │
│←──────────────────────│ │
│ │ │
│ 5. 用 Token 访问资源 │ │
│──────────────────────────────────────────────────→│
│ │ │
│ 6. 返回资源 │ │
│←─────────────────────────────────────────────────│
// 使用 passport.js 实现 OAuth2
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
// 查找或创建用户
User.findOrCreate({ googleId: profile.id }, (err, user) => {
return done(err, user);
});
}));
// 路由
app.get('/auth/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/');
}
);
9.6 API Key 认证
传递方式
# 方式 1:查询参数
curl "https://api.example.com/data?api_key=YOUR_API_KEY"
# 方式 2:自定义头部
curl -H "X-API-Key: YOUR_API_KEY" https://api.example.com/data
# 方式 3:Authorization 头
curl -H "Authorization: ApiKey YOUR_API_KEY" https://api.example.com/data
服务端实现
const crypto = require('crypto');
// 生成 API Key
function generateApiKey() {
return `pk_${crypto.randomBytes(32).toString('hex')}`;
}
// API Key 验证中间件
async function apiKeyAuth(req, res, next) {
const apiKey = req.headers['x-api-key'] || req.query.api_key;
if (!apiKey) {
return res.status(401).json({
error: { code: 'MISSING_API_KEY', message: '请提供 API Key' }
});
}
const keyRecord = await findApiKey(apiKey);
if (!keyRecord) {
return res.status(401).json({
error: { code: 'INVALID_API_KEY', message: 'API Key 无效' }
});
}
if (keyRecord.expires_at && new Date(keyRecord.expires_at) < new Date()) {
return res.status(401).json({
error: { code: 'API_KEY_EXPIRED', message: 'API Key 已过期' }
});
}
req.apiKey = keyRecord;
next();
}
9.7 认证方案对比
| 方案 | 安全性 | 可伸缩性 | 适用场景 |
|---|
| Basic | 低 | 高 | 内部工具、调试 |
| API Key | 中 | 高 | 公开 API |
| Bearer Token (JWT) | 高 | 高 | 现代 Web 应用 |
| Session + Cookie | 中 | 需共享存储 | 传统 Web |
| OAuth 2.0 | 最高 | 高 | 第三方登录 |
9.8 业务场景:多层认证架构
// 路由级别的不同认证策略
const express = require('express');
const app = express();
// 公开路由 — 无需认证
app.get('/api/public/products', (req, res) => {
res.json({ products: [...] });
});
// API Key 认证 — 第三方集成
app.get('/api/v1/external', apiKeyAuth, (req, res) => {
res.json({ data: [...] });
});
// JWT 认证 — 前端应用
app.get('/api/users/me', jwtAuth, (req, res) => {
res.json({ user: req.user });
});
// 角色授权 — 管理员
app.get('/api/admin/users', jwtAuth, requireRole('admin'), (req, res) => {
res.json({ users: [...] });
});
⚠️ 注意事项
- 始终使用 HTTPS:所有认证方案都必须配合 TLS
- JWT 不要存敏感信息:Payload 只是 Base64 编码
- Token 过期:Access Token 短生命周期,配合 Refresh Token
- 不要在 URL 中传递 Token:会被日志记录
- 限制 API Key 权限:遵循最小权限原则
- 记录认证日志:登录、Token 刷新、认证失败都要记录
🔗 扩展阅读
下一章:第 10 章:跨域资源共享 CORS — CORS 机制、预检请求、配置实践