第一章:为什么需要专门的 API 认证?
传统基于 Session 的 Web 登录在前后端分离场景下存在明显缺陷:
| 问题 | 说明 |
|---|---|
| 跨域限制 | Cookie 默认不跨域,需复杂配置 |
| 无状态缺失 | Session 存储在服务端,扩展性差 |
| 移动端不适配 | App 无法自动管理 Cookie |
| 安全性耦合 | CSRF 防护对 API 无意义 |
JWT(JSON Web Token)优势:
- 无状态:Token 自包含用户信息,服务端无需存储
- 跨平台:通过 HTTP Header 传递,天然支持跨域
- 可扩展:可嵌入角色、权限、过期时间等声明
- 标准化:RFC 7519,生态工具丰富
典型流程:
Client POST /api/login → Server 返回 { "token": "xxx" } Client 后续请求 → Header: Authorization: Bearer xxx Server 验证 Token → 执行业务逻辑
第二章:环境准备与依赖安装
2.1 安装新依赖
pip install Flask-RESTful PyJWT marshmallow flask-cors
更新 requirements.txt:
Flask-RESTful==0.3.10
PyJWT==2.8.0
marshmallow==3.21.0
flask-cors==4.0.0
说明:
Flask-RESTful:快速构建 REST API(可选,也可纯 Flask)PyJWT:生成与验证 JWTmarshmallow:序列化/反序列化数据(替代手动 dict 构造)flask-cors:解决跨域问题
2.2 初始化扩展
更新 extensions.py:
# extensions.py
from flask_restful import Api
from flask_cors import CORS
# ... 其他已有扩展 ...
# === 新增:API 与 CORS ===
api = Api() # Flask-RESTful 实例
cors = CORS() # 跨域支持
在 app.py 中注册:
# app.py
from extensions import api, cors # 新增
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# ... 其他初始化 ...
api.init_app(app) # 注册 API
cors.init_app(app, supports_credentials=True) # 启用 CORS
return app
CORS 配置 :
supports_credentials=True允许前端携带凭证(如 Cookies),但 JWT 通常不需要。
第三章:设计 RESTful API 规范
3.1 资源命名与 HTTP 方法
以 任务(Task) 为例:
| 操作 | URL | 方法 | 说明 |
|---|---|---|---|
| 获取任务列表 | /api/tasks |
GET | 分页、过滤 |
| 创建任务 | /api/tasks |
POST | 提交 JSON 数据 |
| 获取单个任务 | /api/tasks/<id> |
GET | 返回详细信息 |
| 更新任务 | /api/tasks/<id> |
PUT/PATCH | 全量/部分更新 |
| 删除任务 | /api/tasks/<id> |
DELETE | 软删除 |
3.2 统一响应格式
所有 API 返回统一结构:
{
"code": 200,
"message": "success",
"data": { ... } // 或 null
}
错误示例:
{
"code": 401,
"message": "Invalid token",
"data": null
}
3.3 错误码设计
| 状态码 | 含义 |
|---|---|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未认证(Token 缺失/无效) |
| 403 | 无权限(如操作他人任务) |
| 404 | 资源不存在 |
| 429 | 请求过于频繁 |
第四章:实现 JWT 认证体系
4.1 生成与验证 Token 工具
新建 utils/jwt_helper.py:
# utils/jwt_helper.py
import jwt
from datetime import datetime, timedelta
from flask import current_app
def generate_token(user_id, expires_in=3600):
"""生成 JWT Token"""
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(seconds=expires_in),
'iat': datetime.utcnow()
}
return jwt.encode(
payload,
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
def verify_token(token):
"""验证并解析 Token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except jwt.ExpiredSignatureError:
return None # Token 过期
except jwt.InvalidTokenError:
return None # Token 无效
4.2 创建登录 API
新建 api/auth.py:
# api/auth.py
from flask_restful import Resource, reqparse
from models import User
from utils.jwt_helper import generate_token
from .base import APIResponse
parser = reqparse.RequestParser()
parser.add_argument('username', type=str, required=True, help='用户名不能为空')
parser.add_argument('password', type=str, required=True, help='密码不能为空')
class LoginAPI(Resource):
def post(self):
args = parser.parse_args()
user = User.query.filter_by(username=args['username']).first()
if not user or not user.check_password(args['password']):
return APIResponse.error(401, '用户名或密码错误')
token = generate_token(user.id)
return APIResponse.success({
'token': token,
'expires_in': 3600,
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
})
4.3 统一响应封装
新建 api/base.py:
# api/base.py
class APIResponse:
@staticmethod
def success(data=None, message='success', code=200):
return {
'code': code,
'message': message,
'data': data
}, code
@staticmethod
def error(code, message, data=None):
return {
'code': code,
'message': message,
'data': data
}, code
4.4 注册 API 路由
在 app.py 或单独 api/__init__.py 中:
# api/__init__.py
from flask_restful import Api
from .auth import LoginAPI
def init_api(api):
api.add_resource(LoginAPI, '/api/login')
在 app.py 中调用:
# app.py
from api import init_api
def create_app(...):
# ...
init_api(api) # 注册所有 API
return app
测试登录:
curl -X POST http://localhost:5000/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin", "password":"123456"}'
第五章:JWT 认证装饰器与权限控制
5.1 实现认证装饰器
新建 decorators/auth.py:
# decorators/auth.py
from functools import wraps
from flask import request, g
from utils.jwt_helper import verify_token
from api.base import APIResponse
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return APIResponse.error(401, '缺少认证令牌')
token = auth_header.split(" ")[1]
user_id = verify_token(token)
if not user_id:
return APIResponse.error(401, '无效或过期的令牌')
g.current_user_id = user_id # 存入 g 对象供后续使用
return f(*args, **kwargs)
return decorated
5.2 集成到任务 API
新建 api/tasks.py:
# api/tasks.py
from flask_restful import Resource, reqparse
from decorators.auth import token_required
from models import Task, db
from api.base import APIResponse
from marshmallow import Schema, fields
# === 数据序列化 Schema ===
class TaskSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.Str(required=True)
description = fields.Str()
done = fields.Bool()
created_at = fields.DateTime(dump_only=True)
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)
# === 解析器 ===
parser = reqparse.RequestParser()
parser.add_argument('title', type=str, required=True, help='标题不能为空')
parser.add_argument('description', type=str)
parser.add_argument('done', type=bool, default=False)
class TaskListAPI(Resource):
method_decorators = [token_required] # 应用认证装饰器
def get(self):
tasks = Task.query.filter_by(user_id=g.current_user_id).all()
return APIResponse.success(tasks_schema.dump(tasks))
def post(self):
args = parser.parse_args()
task = Task(
title=args['title'],
description=args['description'],
done=args['done'],
user_id=g.current_user_id
)
db.session.add(task)
db.session.commit()
return APIResponse.success(task_schema.dump(task), '任务创建成功', 201)
class TaskAPI(Resource):
method_decorators = [token_required]
def get(self, task_id):
task = Task.query.filter_by(id=task_id, user_id=g.current_user_id).first_or_404()
return APIResponse.success(task_schema.dump(task))
def put(self, task_id):
task = Task.query.filter_by(id=task_id, user_id=g.current_user_id).first_or_404()
args = parser.parse_args()
task.title = args['title']
task.description = args['description']
task.done = args['done']
db.session.commit()
return APIResponse.success(task_schema.dump(task))
def delete(self, task_id):
task = Task.query.filter_by(id=task_id, user_id=g.current_user_id).first_or_404()
db.session.delete(task)
db.session.commit()
return APIResponse.success(None, '任务已删除')
关键安全点:
- 所有查询均加
user_id=g.current_user_id→ 数据隔离- 使用
first_or_404()→ 避免泄露 ID 是否存在
5.3 注册任务 API
更新 api/__init__.py:
from .tasks import TaskListAPI, TaskAPI
def init_api(api):
api.add_resource(LoginAPI, '/api/login')
api.add_resource(TaskListAPI, '/api/tasks')
api.add_resource(TaskAPI, '/api/tasks/<int:task_id>')
第六章:API 限流与安全加固
6.1 为 API 添加频率限制
复用第7篇的 Flask-Limiter,按 Token 限流更精准。
修改 decorators/auth.py:
# 在 token_required 中增加限流键
g.rate_limit_key = f"api:{user_id}" # 或 f"api:{request.remote_addr}"
在 api/tasks.py 中应用限流:
from extensions import limiter
class TaskListAPI(Resource):
# ...
pass
# 单独为方法添加限流
limiter.limit("100 per hour", key_func=lambda: g.get('rate_limit_key', request.remote_addr))(
TaskListAPI.get
)
limiter.limit("10 per minute", key_func=lambda: g.get('rate_limit_key', request.remote_addr))(
TaskListAPI.post
)
策略:
- GET:宽松(100次/小时)
- POST/PUT/DELETE:严格(10次/分钟)
6.2 敏感操作二次验证(可选)
对于删除等高危操作,可要求提供 短期有效 Token 或 MFA 验证码。
示例(伪代码):
def delete(self, task_id):
# 检查是否启用 MFA
user = User.query.get(g.current_user_id)
if user.mfa_enabled:
mfa_token = request.headers.get('X-MFA-Token')
if not pyotp.TOTP(user.mfa_secret).verify(mfa_token):
return APIResponse.error(401, 'MFA 验证失败')
# ... 执行删除 ...
第七章:Token 刷新与吊销机制
7.1 为什么需要 Token 刷新?
- Access Token 短期有效(如 1 小时)→ 安全
- Refresh Token 长期有效(如 7 天)→ 用户无需频繁登录
7.2 实现双 Token 体系
7.2.1 修改登录 API
# api/auth.py - LoginAPI
access_token = generate_token(user.id, expires_in=3600)
refresh_token = generate_token(user.id, expires_in=7*24*3600) # 7天
return APIResponse.success({
'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': 3600
})
7.2.2 创建刷新 Token API
# api/auth.py
refresh_parser = reqparse.RequestParser()
refresh_parser.add_argument('refresh_token', type=str, required=True)
class RefreshTokenAPI(Resource):
def post(self):
args = refresh_parser.parse_args()
user_id = verify_token(args['refresh_token'])
if not user_id:
return APIResponse.error(401, 'Refresh token 无效')
new_access_token = generate_token(user_id, expires_in=3600)
return APIResponse.success({'access_token': new_access_token})
注册路由:
api.add_resource(RefreshTokenAPI, '/api/token/refresh')
7.3 Token 吊销(黑名单)
当用户登出或怀疑泄露时,需立即使 Token 失效。
7.3.1 使用 Redis 存储黑名单
# utils/jwt_helper.py
from extensions import redis_client
def revoke_token(token, expires_in=3600):
"""将 Token 加入黑名单"""
redis_client.setex(f"blacklist:{token}", expires_in, "1")
def is_token_blacklisted(token):
return redis_client.exists(f"blacklist:{token}")
7.3.2 修改 verify_token
def verify_token(token):
if is_token_blacklisted(token):
return None
# ... 原有验证逻辑 ...
7.3.3 添加登出 API
class LogoutAPI(Resource):
@token_required
def post(self):
auth_header = request.headers.get('Authorization')
token = auth_header.split(" ")[1]
revoke_token(token, expires_in=3600) # 黑名单保留1小时
return APIResponse.success(None, '已登出')
第八章:前端调用示例(Vue/React)
8.1 Axios 拦截器配置
// api.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000/api',
timeout: 10000
});
// 请求拦截器:自动添加 Token
api.interceptors.request.use(config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:处理 401
api.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
try {
// 尝试刷新 Token
const res = await axios.post('/api/token/refresh', { refresh_token: refreshToken });
const newToken = res.data.data.access_token;
localStorage.setItem('access_token', newToken);
// 重试原请求
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios(error.config);
} catch (refreshError) {
// 刷新失败,跳转登录
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
export default api;
8.2 调用任务 API
// 获取任务列表
api.get('/tasks').then(res => {
console.log(res.data.data);
});
// 创建任务
api.post('/tasks', { title: '新任务', description: '...' });
第九章:测试 API 安全性
9.1 测试认证绕过
def test_task_without_token(client):
response = client.get('/api/tasks')
assert response.json['code'] == 401
def test_task_with_invalid_token(client):
headers = {'Authorization': 'Bearer invalid.token.here'}
response = client.get('/api/tasks', headers=headers)
assert response.json['code'] == 401
9.2 测试数据隔离
def test_user_cannot_access_others_tasks(client, user1, user2):
# user1 创建任务
token1 = login_and_get_token(client, user1)
client.post('/api/tasks', json={'title': 'User1 Task'},
headers={'Authorization': f'Bearer {token1}'})
# user2 尝试访问
token2 = login_and_get_token(client, user2)
response = client.get('/api/tasks', headers={'Authorization': f'Bearer {token2}'})
assert len(response.json['data']) == 0 # 不应看到 user1 的任务
第十章:生产环境最佳实践
| 项目 | 建议 |
|---|---|
| HTTPS | 强制使用,防止 Token 被窃听 |
| Token 存储 | 前端:HttpOnly Cookie(防 XSS)或内存(防 CSRF) |
| 密钥管理 | SECRET_KEY 从环境变量加载,定期轮换 |
| 日志审计 | 记录 API 调用者、IP、操作类型 |
| 速率限制 | 按用户 ID 限流,避免 IP 限流被绕过 |
HttpOnly Cookie 方案(更高安全):
- 登录成功后,将 Token 写入
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict- 前端无法读取,但浏览器自动携带
- 需关闭 CORS credentials 或调整策略