ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Python的Web服务统一身份认证协议设计与实现:OAuth2简化模式+JWT令牌

基于Python的Web服务统一身份认证协议设计与实现:OAuth2简化模式+JWT令牌

简介:这份资源是面向高校计算机相关专业毕业设计的完整项目包,主题为基于Python与Django的Web服务统一身份认证协议设计,适合正在准备毕设或需要学习单点登录与身份认证实现的学生及开发者参考。项目采用B/S架构,后端基于Django框架,配合MySQL数据库完成数据持久化,核心功能涵盖用户信息管理(查看、修改、删除)以及单点登录机制,通过一次服务器认证即可访问相互信任的站点,并按权限分配策略实现自动登录与差异化访问。压缩包共603个文件,约8.34MB,包含105个py源码文件、13个html页面、52个js脚本、21个css样式以及大量png、gif等界面素材,另附数据库文件、说明文档与演示视频,便于理解整体结构与运行流程。目前已有904人学习下载,可作为毕设选题落地的参考方案,帮助读者快速掌握统一身份认证系统的设计思路与关键实现细节。

1. 从一份毕业设计压缩包说起:Web 服务统一身份认证到底在解决什么

你拿到手的这个(基于python的毕业设计)web服务统一身份认证协议设计(源码+说明+演示视频).zip,名字很长,但核心就一件事:让多个 Web 服务共用一套登录态,而不是每个子系统各存一份用户名密码。做过课程设计的人都懂,教务系统、图书馆、选课平台各登一次,密码还要求不一样,这就是典型的身份孤岛。统一身份认证协议要做的,是把「你是谁」这件事从各个业务系统里抽出来,交给一个独立的认证中心,业务系统只负责拿着令牌去问「这个人合法吗」。

这个方向适合两类人:一类是正在做计算机毕业设计、需要一套能跑起来、能讲清楚原理、还能录演示视频的完整方案;另一类是想理解 SSO(单点登录)和令牌校验机制、但不想一上来就啃 CAS 或 OAuth2 全量规范的开发者。标题里的「协议设计」不是让你去发明新协议,而是让你基于现有成熟协议(常见做法是 OAuth2 的授权码模式或简化版 CAS)做一套可落地的实现。Python 在这里的角色是认证中心服务端和客户端 SDK 的实现语言,选它是因为生态里 Flask、requests、PyJWT 这些库足够把协议流程串起来,代码量可控,答辩时也讲得清。

我见过太多毕业设计卡在「能演示但说不清协议流程」这一步。演示视频里点一下登录就跳转了,老师问一句「令牌怎么防篡改」就答不上来。所以这篇笔记不按论文目录走,而是按一个真实可复现的落地路径来拆:先讲清协议选型和令牌设计,再动手把认证中心、业务客户端、令牌校验三块跑通,最后把踩过的坑和进阶验证方法摊开。你照着做,至少能拿到一个能讲、能改、能扩展的工程骨架。

2. 协议选型与令牌设计:为什么不用 Session 共享,而用令牌

2.1 三种常见方案的取舍:Session 共享、CAS、OAuth2 简化模式

做 Web 服务统一身份认证,绕不开的第一个决策就是「登录态存在哪、怎么传」。最省事的做法是 Session 共享,把多个服务的 Session 存到同一个 Redis 里,浏览器带着同一个 Cookie 就能识别。这个方案在单机或内网小规模场景下确实能用,但它有两个硬伤:一是跨域场景下 Cookie 的 SameSite 和 Domain 限制会让你反复翻车;二是业务系统直接依赖 Session 存储,认证中心和业务系统耦合太深,扩展性差。毕业设计如果只做 Session 共享,答辩时很难体现「协议设计」四个字。

CAS(Central Authentication Service)是经典的单点登录协议,流程清晰:用户访问业务系统,业务系统发现没登录就重定向到 CAS 服务端,服务端认证后带 ticket 跳回,业务系统拿 ticket 去服务端校验。它的优点是协议简单、票据一次性、安全性好;缺点是移动端和前后端分离场景下重定向流程比较别扭,而且 CAS 的 ticket 校验是服务端到服务端的同步调用,高并发时认证中心压力集中。

OAuth2 的授权码模式是当前更通用的选择,虽然它本意是授权而非认证,但加上openid或自定义用户信息接口后,完全可以承担统一身份认证的职责。它的核心是:业务系统引导用户到认证中心,用户认证后认证中心返回授权码,业务系统用授权码换令牌,再用令牌获取用户信息。这个流程前后端分离友好,令牌可以自包含(JWT),业务系统本地就能校验,不必每次都回认证中心。

对于毕业设计,我一般建议选 OAuth2 授权码模式的简化版:保留授权码换令牌的核心流程,令牌用 JWT 自包含,业务系统本地校验签名和过期时间。这样既体现了协议设计,又不会因为引入完整 OAuth2 的刷新令牌、作用域、同意页而把代码量撑到失控。

2.2 JWT 令牌的结构与签名参数怎么定

选 JWT 作为令牌载体,是因为它把用户标识、过期时间、签发者都编码进令牌本身,业务系统不需要查库就能完成校验。一个典型的 JWT 由三部分组成:Header、Payload、Signature,用点号连接。Header 声明签名算法,Payload 放声明(claims),Signature 是对前两部分的签名。

下面是一个用 Python 生成 JWT 的最小代码,依赖PyJWT库:

import jwt import time # 签名密钥,生产环境必须从环境变量读取,不能硬编码 SECRET_KEY = "your-256-bit-secret-change-in-production" # 签名算法,HS256 是对称加密,认证中心和业务系统共享密钥 ALGORITHM = "HS256" def create_token(user_id: str, username: str, expire_seconds: int = 3600) -> str: """ 生成 JWT 令牌 :param user_id: 用户唯一标识 :param username: 用户名,用于业务系统展示 :param expire_seconds: 过期时间,默认 1 小时 :return: 编码后的 JWT 字符串 """ now = int(time.time()) payload = { "iss": "auth-center", # 签发者,业务系统可校验 "sub": user_id, # 主题,通常是用户 ID "name": username, # 自定义声明,用户展示名 "iat": now, # 签发时间 "exp": now + expire_seconds, # 过期时间,必须设置 "jti": f"{user_id}-{now}" # 令牌唯一 ID,可用于黑名单 } token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) return token def verify_token(token: str) -> dict: """ 校验 JWT 令牌,失败会抛异常 :param token: 待校验的令牌 :return: 解码后的 payload """ try: payload = jwt.decode( token, SECRET_KEY, algorithms=[ALGORITHM], issuer="auth-center", # 校验签发者 options={"require": ["exp", "sub", "iss"]} # 强制要求这些声明存在 ) return payload except jwt.ExpiredSignatureError: raise ValueError("令牌已过期") except jwt.InvalidTokenError as e: raise ValueError(f"令牌无效: {e}")

这段代码里几个参数值得展开说。SECRET_KEY的长度和随机性直接决定签名强度,HS256 要求密钥至少 256 位,也就是 32 字节。很多毕业设计里直接写"secret"或者"123456",这在答辩时是明显的扣分点,因为攻击者拿到密钥就能伪造任意用户令牌。exp过期时间必须设置,不设过期时间的令牌一旦泄露就是永久后门。iss签发者声明用于业务系统确认令牌确实来自认证中心,而不是其他服务签发的。jti是令牌唯一 ID,如果你要做登出或强制下线,就需要维护一个黑名单,把未过期但已登出的jti记下来,校验时额外查一次。

提示:HS256 是对称算法,认证中心和所有业务系统共享同一个密钥。如果业务系统数量多、部署环境不可控,建议换成 RS256 非对称算法,认证中心用私钥签名,业务系统用公钥验签,私钥不出认证中心。

2.3 授权码换令牌的完整时序与接口约定

协议流程要落地,必须把接口约定清楚。简化版 OAuth2 授权码模式涉及四个角色:用户浏览器、业务系统(客户端)、认证中心、用户信息接口。核心时序是:

  1. 用户访问业务系统受保护页面,业务系统发现无令牌,重定向到认证中心授权页,带上client_id和redirect_uri。
  2. 用户在认证中心登录,认证中心生成一次性授权码code,重定向回redirect_uri?code=xxx。
  3. 业务系统后端拿code调用认证中心令牌接口,换取access_token。
  4. 业务系统用access_token调用用户信息接口,拿到用户标识,建立本地会话或直接返回给前端。

接口约定用表格列清楚,避免前后端联调时扯皮:

接口方法路径关键参数返回
授权页GET/oauth/authorizeclient_id, redirect_uri, state重定向带 code
令牌接口POST/oauth/tokencode, client_id, client_secretaccess_token, token_type, expires_in
用户信息GET/oauth/userinfoAuthorization: Bearer tokenuser_id, username
令牌校验POST/oauth/verifytokenvalid, payload

state参数是防 CSRF 的关键,业务系统生成随机字符串存在本地,回调时比对,不一致就拒绝。client_secret是业务系统的凭证,必须后端保存,绝不能出现在前端代码里。授权码code必须一次性使用,用过即失效,且有效期建议不超过 5 分钟。

3. 用 Flask 把认证中心跑起来:路由、授权码与令牌签发

3.1 认证中心的最小工程结构与依赖

认证中心是整个方案的核心,它负责用户登录、授权码生成、令牌签发和用户信息提供。用 Flask 来实现,是因为它轻量、路由清晰、和 PyJWT 配合简单。工程结构不需要复杂,毕业设计阶段四个文件足够:

auth-center/ ├── app.py # 主应用,路由和业务逻辑 ├── models.py # 用户模型和授权码存储 ├── config.py # 配置项,密钥、过期时间 └── requirements.txt # 依赖清单

依赖清单里核心是三个库:Flask提供 Web 框架,PyJWT负责令牌编解码,flask-cors处理跨域(前后端分离时业务系统域名和认证中心域名不同)。安装命令:

pip install Flask PyJWT flask-cors

config.py里把密钥、授权码有效期、令牌有效期集中管理,避免散落在各处:

import os class Config: # 生产环境从环境变量读取,开发环境给默认值 SECRET_KEY = os.environ.get("AUTH_SECRET_KEY", "dev-secret-key-32-bytes-long!!!") JWT_ALGORITHM = "HS256" # 授权码有效期,单位秒 AUTH_CODE_EXPIRE = 300 # 令牌有效期,单位秒 TOKEN_EXPIRE = 3600 # 允许的业务系统 client_id 和 secret 映射 CLIENTS = { "web-app-1": {"secret": "app1-secret", "redirect_uris": ["http://localhost:5001/callback"]}, "web-app-2": {"secret": "app2-secret", "redirect_uris": ["http://localhost:5002/callback"]}, }

CLIENTS字典模拟了业务系统注册表,每个业务系统有独立的client_id和client_secret,以及允许的回调地址白名单。回调地址白名单是防开放重定向的关键,如果不对redirect_uri做校验,攻击者可以构造恶意回调地址窃取授权码。

3.2 授权码的生成、存储与一次性消费

授权码是连接授权页和令牌接口的临时凭证,它的安全性要求是:随机、短有效期、一次性。用 Python 的secrets模块生成随机码,比random模块更适合安全场景。

import secrets import time from threading import Lock class AuthCodeStore: """授权码存储,内存实现,生产环境应换成 Redis""" def __init__(self, expire_seconds=300): self._store = {} # code -> {user_id, client_id, expire_at} self._lock = Lock() self._expire = expire_seconds def generate(self, user_id: str, client_id: str) -> str: """生成授权码并存储""" code = secrets.token_urlsafe(32) # 32 字节随机,URL 安全 with self._lock: self._store[code] = { "user_id": user_id, "client_id": client_id, "expire_at": time.time() + self._expire } return code def consume(self, code: str, client_id: str) -> str: """ 消费授权码,返回 user_id 一次性:取出即删除;校验 client_id 和过期时间 """ with self._lock: record = self._store.pop(code, None) if record is None: raise ValueError("授权码不存在或已使用") if record["client_id"] != client_id: raise ValueError("授权码与客户端不匹配") if time.time() > record["expire_at"]: raise ValueError("授权码已过期") return record["user_id"]

secrets.token_urlsafe(32)生成的是 32 字节随机数的 URL 安全 Base64 编码,长度约 43 个字符,暴力猜测不可行。consume方法里pop操作保证了一次性:无论后续校验是否通过,授权码都已经被移除,攻击者无法重放。client_id校验防止一个业务系统拿另一个业务系统的授权码去换令牌。过期时间校验放在最后,是因为即使授权码泄露,5 分钟后也自动失效。

3.3 令牌接口与用户信息接口的实现

令牌接口接收code、client_id、client_secret,校验通过后签发 JWT。用户信息接口从Authorization头里取令牌,校验后返回用户信息。

from flask import Flask, request, jsonify, redirect from config import Config from models import AuthCodeStore, UserStore import jwt import time app = Flask(__name__) app.config.from_object(Config) code_store = AuthCodeStore(expire_seconds=app.config["AUTH_CODE_EXPIRE"]) user_store = UserStore() @app.route("/oauth/token", methods=["POST"]) def token(): """授权码换令牌接口""" data = request.get_json() code = data.get("code") client_id = data.get("client_id") client_secret = data.get("client_secret") # 校验客户端身份 client = app.config["CLIENTS"].get(client_id) if not client or client["secret"] != client_secret: return jsonify({"error": "invalid_client"}), 401 # 消费授权码 try: user_id = code_store.consume(code, client_id) except ValueError as e: return jsonify({"error": str(e)}), 400 # 签发令牌 now = int(time.time()) payload = { "iss": "auth-center", "sub": user_id, "iat": now, "exp": now + app.config["TOKEN_EXPIRE"], "jti": f"{user_id}-{now}" } access_token = jwt.encode(payload, app.config["SECRET_KEY"], algorithm=app.config["JWT_ALGORITHM"]) return jsonify({ "access_token": access_token, "token_type": "Bearer", "expires_in": app.config["TOKEN_EXPIRE"] }) @app.route("/oauth/userinfo", methods=["GET"]) def userinfo(): """用户信息接口,从 Bearer 令牌中解析用户""" auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return jsonify({"error": "missing_token"}), 401 token = auth_header[7:] try: payload = jwt.decode( token, app.config["SECRET_KEY"], algorithms=[app.config["JWT_ALGORITHM"]], issuer="auth-center" ) except jwt.ExpiredSignatureError: return jsonify({"error": "token_expired"}), 401 except jwt.InvalidTokenError: return jsonify({"error": "invalid_token"}), 401 user = user_store.get(payload["sub"]) if not user: return jsonify({"error": "user_not_found"}), 404 return jsonify({"user_id": user["id"], "username": user["username"]})

令牌接口里,客户端身份校验放在最前面,避免无效请求消耗授权码。jwt.encode的 payload 里sub是用户 ID,jti用于后续黑名单。用户信息接口的Authorization头格式必须是Bearer <token>,解析时注意空格。jwt.decode的issuer参数会校验iss声明,防止其他服务签发的令牌混入。

注意:内存存储的授权码和用户数据在服务重启后会丢失,毕业设计演示时如果中途重启服务,需要重新登录。生产环境或需要持久化的场景,把AuthCodeStore换成 Redis 实现,UserStore换成数据库查询。

4. 业务系统接入:客户端 SDK 与令牌本地校验

4.1 业务系统如何发起授权跳转与处理回调

业务系统接入统一身份认证,核心是三件事:未登录时跳转认证中心、处理回调拿令牌、本地校验令牌。用 Flask 写一个业务系统的最小实现:

from flask import Flask, request, redirect, session, jsonify import requests import jwt import secrets app = Flask(__name__) app.secret_key = "business-app-session-secret" AUTH_CENTER = "http://localhost:5000" CLIENT_ID = "web-app-1" CLIENT_SECRET = "app1-secret" REDIRECT_URI = "http://localhost:5001/callback" # 与认证中心共享的 JWT 密钥,用于本地校验 JWT_SECRET = "dev-secret-key-32-bytes-long!!!" JWT_ALGORITHM = "HS256" @app.route("/protected") def protected(): """受保护页面,未登录则跳转认证中心""" token = session.get("access_token") if not token: # 生成 state 防 CSRF state = secrets.token_urlsafe(16) session["oauth_state"] = state auth_url = ( f"{AUTH_CENTER}/oauth/authorize" f"?client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URI}" f"&state={state}" f"&response_type=code" ) return redirect(auth_url) # 本地校验令牌 try: payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM], issuer="auth-center") return jsonify({"message": "访问成功", "user_id": payload["sub"]}) except jwt.ExpiredSignatureError: session.pop("access_token", None) return redirect("/protected") except jwt.InvalidTokenError: return jsonify({"error": "invalid_token"}), 401 @app.route("/callback") def callback(): """认证中心回调,用 code 换令牌""" code = request.args.get("code") state = request.args.get("state") # 校验 state if not state or state != session.pop("oauth_state", None): return jsonify({"error": "state_mismatch"}), 400 # 用 code 换令牌 resp = requests.post(f"{AUTH_CENTER}/oauth/token", json={ "code": code, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET }) if resp.status_code != 200: return jsonify({"error": "token_exchange_failed"}), 400 data = resp.json() session["access_token"] = data["access_token"] return redirect("/protected")

/protected路由先检查 session 里有没有令牌,没有就生成state并跳转。state存在 session 里,回调时比对,防止攻击者伪造回调请求。/callback路由拿到code后,用requests.post调用认证中心令牌接口,注意这里是后端到后端的调用,client_secret不会暴露给浏览器。拿到令牌后存入 session,重定向回受保护页面。

4.2 本地校验与远程校验的边界:什么时候必须回认证中心

业务系统拿到 JWT 后,有两种校验方式:本地校验和远程校验。本地校验就是用共享密钥或公钥验证签名和过期时间,不访问认证中心;远程校验是调用认证中心的/oauth/verify接口。

本地校验的优点是快、无网络开销,适合高频接口。但它有两个边界:一是无法感知令牌被提前吊销(比如用户登出或管理员强制下线),因为 JWT 自包含,签发后到过期前一直有效;二是如果密钥泄露,所有业务系统都受影响。

远程校验能解决吊销问题,但每次请求都调认证中心,认证中心会成为瓶颈。折中方案是:本地校验为主,维护一个短期的吊销黑名单(比如 Redis 里存被吊销的jti,过期时间与令牌一致),本地校验通过后再查一次黑名单。黑名单查询是内存操作,比远程调用轻量得多。

import redis redis_client = redis.Redis(host="localhost", port=6379, db=0) def is_token_revoked(jti: str) -> bool: """检查令牌是否在黑名单中""" return redis_client.exists(f"revoked:{jti}") == 1 def revoke_token(jti: str, expire_seconds: int): """将令牌加入黑名单,过期时间与令牌剩余有效期一致""" redis_client.setex(f"revoked:{jti}", expire_seconds, "1")

setex的过期时间很关键,如果设得太长,黑名单会无限增长;设得太短,令牌还没过期黑名单就失效了。正确做法是设为令牌的剩余有效期,令牌自然过期后黑名单自动清理。

4.3 多业务系统接入时的配置管理

当业务系统从两个变成五个、十个,配置管理就成了问题。每个业务系统都要配CLIENT_ID、CLIENT_SECRET、AUTH_CENTER地址、JWT_SECRET,散落在各处的配置文件里,改一个密钥要改所有系统。

常见做法是抽一个共享的配置中心或环境变量模板。毕业设计阶段不需要上配置中心,但至少要做到:认证中心的CLIENTS注册表是唯一权威来源,业务系统的client_secret从环境变量读取,不写死在代码里。下面是一个环境变量模板:

# 业务系统环境变量 export AUTH_CENTER_URL="http://localhost:5000" export OAUTH_CLIENT_ID="web-app-1" export OAUTH_CLIENT_SECRET="app1-secret" export OAUTH_REDIRECT_URI="http://localhost:5001/callback" export JWT_SECRET="dev-secret-key-32-bytes-long!!!"

业务系统代码里用os.environ.get读取,本地开发用.env文件加载,生产环境由部署平台注入。这样认证中心轮换密钥时,只需要更新环境变量并重启业务系统,不需要改代码。

5. 避坑与排查:授权码、令牌、跨域和时钟的五个血泪教训

5.1 授权码被重复使用导致令牌重复签发

现象:同一个code连续调用两次/oauth/token,两次都返回了有效的access_token。

原因:授权码存储用了字典查询但没有删除,或者删除操作在异常分支里被跳过。有些实现先校验client_id和过期时间,校验通过后才删除,如果校验失败就不删除,攻击者可以反复尝试。

解决:consume方法里用pop而不是get,取出即删除,无论后续校验是否通过都不再保留。校验顺序调整为:先pop取出,再校验client_id和过期时间,校验失败直接抛异常,授权码已经不在存储里了。

5.2 令牌过期后业务系统陷入重定向循环

现象:令牌过期后,业务系统跳转认证中心,认证中心发现用户已登录直接回调,业务系统拿到新令牌,但页面还是提示未登录,反复跳转。

原因:业务系统在/callback里把令牌存入了 session,但/protected里校验令牌失败后只清除了 session 里的令牌,没有清除认证中心的登录态。认证中心认为用户已登录,直接签发新授权码,但业务系统可能因为state不匹配或 session 写入失败而拿不到令牌。

解决:在/protected里校验失败时,除了清除本地令牌,还要跳转认证中心的登出接口,或者带上prompt=login参数强制重新认证。更简单的做法是确保state生成和校验逻辑正确,session 写入成功后再重定向。

5.3 跨域场景下 Authorization 头丢失

现象:前后端分离时,前端用fetch调用业务系统接口,带上Authorization: Bearer xxx,后端收不到这个头。

原因:跨域请求默认不发送自定义头,浏览器会先发OPTIONS预检请求,如果后端没有正确响应Access-Control-Allow-Headers,实际请求就不会带上Authorization。

解决:后端 CORS 配置里明确允许Authorization头:

from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": ["http://localhost:3000"]}}, allow_headers=["Content-Type", "Authorization"], methods=["GET", "POST", "OPTIONS"])

allow_headers必须包含Authorization,origins不要用*配合credentials,否则浏览器会拒绝。

5.4 服务器时钟偏差导致令牌校验失败

现象:认证中心签发的令牌,在业务系统校验时报ImmatureSignatureError或ExpiredSignatureError,但令牌明明刚签发。

原因:认证中心和业务系统部署在不同机器上,时钟没有同步,偏差超过令牌的iat和exp容差。

解决:部署时用 NTP 同步时钟。代码层面,jwt.decode可以设置leeway参数容忍一定偏差:

jwt.decode(token, SECRET_KEY, algorithms=["HS256"], leeway=10)

leeway=10表示允许 10 秒的时钟偏差。但这不是根治办法,生产环境必须同步时钟。

5.5 回调地址白名单校验不严导致开放重定向

现象:攻击者构造redirect_uri=http://evil.com/callback,认证中心校验通过后把授权码发到了恶意站点。

原因:认证中心只校验了client_id存在,没有校验redirect_uri是否在该客户端的白名单里。

解决:在授权页路由里,取出client_id对应的redirect_uris列表,精确匹配redirect_uri,不匹配就拒绝。不要用startswith做前缀匹配,因为http://localhost:5001.evil.com会绕过http://localhost:5001的前缀检查。精确匹配或解析 URL 后比对 host 和 path。

6. 进阶验证:用脚本模拟攻击面,确认协议真的扛得住

6.1 写一个令牌篡改检测脚本

协议设计得再好,不验证就是纸上谈兵。我一般会写一个小脚本,模拟几种常见攻击,看认证中心能不能挡住。第一种是篡改令牌 payload,把sub改成别的用户 ID,看签名校验是否失败。

import jwt import requests AUTH_CENTER = "http://localhost:5000" SECRET_KEY = "dev-secret-key-32-bytes-long!!!" def test_tampered_token(): """篡改 payload 后签名失效,应被拒绝""" # 用错误密钥签发一个令牌 fake_token = jwt.encode( {"iss": "auth-center", "sub": "admin", "exp": 9999999999}, "wrong-secret", algorithm="HS256" ) resp = requests.get( f"{AUTH_CENTER}/oauth/userinfo", headers={"Authorization": f"Bearer {fake_token}"} ) assert resp.status_code == 401, f"篡改令牌未被拒绝: {resp.status_code}" print("篡改令牌检测通过") def test_expired_token(): """过期令牌应被拒绝""" import time expired_token = jwt.encode( {"iss": "auth-center", "sub": "user1", "exp": int(time.time()) - 100}, SECRET_KEY, algorithm="HS256" ) resp = requests.get( f"{AUTH_CENTER}/oauth/userinfo", headers={"Authorization": f"Bearer {expired_token}"} ) assert resp.status_code == 401, f"过期令牌未被拒绝: {resp.status_code}" print("过期令牌检测通过") def test_reused_code(): """授权码重复使用应失败""" # 先正常获取一个 code(需要模拟登录流程,这里省略) # 然后连续两次调用 token 接口 pass if __name__ == "__main__": test_tampered_token() test_expired_token()

test_tampered_token用错误密钥签发令牌,认证中心用正确密钥校验,签名不匹配返回 401。test_expired_token签发一个exp在过去的令牌,校验时抛ExpiredSignatureError,返回 401。这两个测试跑通,说明签名和过期校验生效。

6.2 授权码一次性与 state 防 CSRF 的验证方法

授权码一次性验证需要模拟完整流程:先登录拿到code,然后用同一个code调两次令牌接口,第二次应该失败。state防 CSRF 验证是构造一个不带state或state不匹配的回调请求,业务系统应该拒绝。

def test_code_single_use(): """授权码只能用一次""" # 假设已经通过登录流程拿到了 code code = "test-code-from-login" # 第一次换令牌 resp1 = requests.post(f"{AUTH_CENTER}/oauth/token", json={ "code": code, "client_id": "web-app-1", "client_secret": "app1-secret" }) # 第二次换令牌,应该失败 resp2 = requests.post(f"{AUTH_CENTER}/oauth/token", json={ "code": code, "client_id": "web-app-1", "client_secret": "app1-secret" }) assert resp1.status_code == 200, "首次换令牌失败" assert resp2.status_code == 400, "授权码被重复使用" print("授权码一次性检测通过")

这个测试的关键是resp2必须返回 400,如果返回 200 说明授权码没有被正确消费。state验证在业务系统的/callback里,构造一个state不匹配的请求,应该返回 400。

6.3 压测令牌接口看认证中心瓶颈在哪

毕业设计答辩时老师可能会问「并发怎么样」,你可以用locust或简单的ab命令压一下令牌接口。但令牌接口依赖授权码,不能直接压,所以压测对象选用户信息接口或令牌校验接口。

# 用 ab 压测用户信息接口,100 并发,1000 次请求 ab -n 1000 -c 100 -H "Authorization: Bearer <valid-token>" http://localhost:5000/oauth/userinfo

关注两个指标:Requests per second和Time per request。如果 QPS 上不去,检查是不是每次请求都在查数据库或做复杂计算。用户信息接口应该只做 JWT 解码和一次用户查询,如果用户查询是瓶颈,加缓存。认证中心的令牌签发接口因为涉及授权码消费和 JWT 编码,QPS 会低一些,但毕业设计场景下通常够用。

我自己的习惯是,每做完一个认证流程,先跑一遍篡改和过期测试,再跑一次授权码一次性测试,最后压一下用户信息接口。这三步走完,协议的基本安全性就有底了。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表