Python Web 开发进阶实战:API 安全与 JWT 认证 —— 构建企业级 RESTful 接口

第一章:为什么需要专门的 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:生成与验证 JWT
  • marshmallow:序列化/反序列化数据(替代手动 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 敏感操作二次验证(可选)

对于删除等高危操作,可要求提供 短期有效 TokenMFA 验证码

示例(伪代码):

复制代码
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 或调整策略
相关推荐
摸鱼的春哥2 小时前
继续AI编排实战:带截图的连麦切片文章生成
前端·javascript·后端
Allen_LVyingbo2 小时前
具备安全护栏与版本化证据溯源的python可审计急诊分诊平台复现
开发语言·python·安全·搜索引擎·知识图谱·健康医疗
出了名的洗发水2 小时前
科技感404页面
前端·科技·html
weixin199701080162 小时前
安家 GO item_get - 获取安家详情数据接口对接全攻略:从入门到精通
java·大数据·python·golang
咔咔一顿操作2 小时前
nvm安装Node后node -v正常,npm -v提示“无法加载文件”问题解决
前端·npm·node.js
Sapphire~2 小时前
【前端基础】03- .stop VS .prevent
前端
zsd_312 小时前
npm指定本地缓存、安装包、仓库路径
前端·缓存·npm·node.js·私服·安装包·本地
半个开心果2 小时前
vue3项目结构里的hooks 和utils
前端·javascript·vue.js
Wzx1980122 小时前
自研开发的前后端项目部署流程
vue.js·python