资讯动态

基于Python的Web统一身份认证协议设计与实现:OAuth2+OIDC实战

发布时间:2026/10/18 19:28:42 来源:尧图企业网站定制
简介这份资源是面向高校计算机相关专业毕业设计的完整项目包主题为基于Python的Web服务统一身份认证协议设计适合正在准备毕设或需要学习Django全栈开发的学生参考。项目采用PythonDjangoMySQL的B/S架构核心实现了用户信息管理与单点登录两大模块前者支持对用户信息的查看、修改与删除后者通过一次服务器认证即可访问相互信任的站点并按权限分配策略自动登录不同网站有效简化重复登录流程。压缩包共603个文件约8.34MB其中105个py源码文件构成后端逻辑52个js与21个css、13个html负责前端交互与页面样式另有大量png、gif图片及pyc缓存、sql与sqlite3数据库文件并附有说明文档与演示视频。目前已有904人学习下载读者可获取完整源码、数据库脚本、运行说明与操作录屏便于快速理解统一身份认证的实现思路并完成毕设复现。1. 统一身份认证到底在解决什么从一个反复登录的下午说起做过 Web 项目的人大概都遇到过这种场景手上有三四个内部系统每个系统一套账号密码运维同事每次加人都要挨个后台建号员工离职又要挨个删。更麻烦的是某个系统改了密码其他系统还是旧密码最后大家干脆全用同一个弱口令安全形同虚设。统一身份认证要解决的就是这件事——让用户只在一处登录其他系统通过一套协议信任这个登录结果不再各自维护账号。这个标题里的「基于 Python 的 Web 服务统一身份认证协议设计」落到工程上通常指两件事一是选一套协议常见是 OAuth2 加 OIDC或者更轻量的 CAS 思路二是用 Python 实现认证中心IdP和接入方SP两侧的交互。它适合正在做毕业设计、需要一套能跑起来、能演示、能讲清原理的完整方案的同学也适合小团队想给内部系统做单点登录的开发者。下面我按「协议怎么立住 → 认证中心怎么写 → 接入方怎么接 → 坑在哪 → 怎么验证」的顺序讲透。2. 协议选型与核心流程为什么是 OAuth2 OIDC 而不是自己造2.1 三种常见方案的取舍统一身份认证在 Web 场景下有三条主流路线选错了后面全是返工。第一种是 CAS 思路认证中心发一张票据ticket接入方拿票据回认证中心校验。它的优点是模型简单接入方几乎不用理解令牌语义适合纯内部、系统数量不多的场景。缺点是移动端和前后端分离场景支持一般扩展性有限。第二种是 SAML企业级老牌方案基于 XML 断言。它的优势是成熟、被大量企业身份提供商支持缺点是 XML 签名校验复杂Python 生态里实现起来心智负担重调试时一个命名空间写错就报签名失败对毕业设计来说性价比不高。第三种是 OAuth2 加 OIDC。OAuth2 本身是授权框架解决「第三方能不能访问我的资源」OIDC 在它之上加了一层身份层用 ID Token一个 JWT承载「你是谁」。这套组合前后端分离、移动端、第三方登录都能覆盖Python 生态成熟是我一般会推荐的方案。方案令牌形式前后端分离友好度Python 生态适合场景CAS票据一般一般纯内部少量系统SAMLXML 断言较差偏弱企业已有 IdPOAuth2OIDCJWT好成熟通用含移动端2.2 授权码模式的关键跳转OIDC 里最该掌握的是授权码模式Authorization Code Flow因为它是唯一同时兼顾安全和体验的流程。整个链路有四个角色用户浏览器、接入方SP也叫客户端、认证中心IdP、资源接口。流程拆开看是这样用户访问接入方受保护页面接入方发现没登录把用户重定向到认证中心的授权端点带上 client_id、redirect_uri、scope、state。用户在认证中心登录并同意授权后认证中心把用户重定向回 redirect_uri并附上一个一次性授权码 code。接入方在服务端用 code 加 client_secret 去认证中心的令牌端点换取 access_token 和 id_token。最后接入方校验 id_token 的签名和声明拿到用户身份建立自己的会话。这里有两个点必须强调。一是 code 换 token 这一步必须在服务端做因为要带 client_secret放前端等于把密钥公开。二是 state 参数必须随机生成并在回调时校验它是防 CSRF 的关键很多人图省事不传结果被跨站请求伪造攻击。2.3 ID Token 里到底有什么id_token 是一个 JWT结构是 header.payload.signature 三段。payload 里常见的声明有 iss签发者、sub用户唯一标识、aud接收方 client_id、exp过期时间、iat签发时间、nonce防重放。校验时这几项一个都不能漏签名要用认证中心公钥验iss 要等于认证中心地址aud 要等于自己的 client_idexp 不能过期nonce 要和发起时存的一致。注意sub 才是用户的稳定唯一标识不要用 email 当主键用户改邮箱后账号就对不上了。3. 用 Python 实现认证中心从密钥到令牌端点3.1 环境与依赖我一般用 Flask 搭认证中心因为它轻、路由直观毕业设计演示足够。依赖装这几个pip install flask pyjwt cryptography requestspyjwt 负责签发和校验 JWTcryptography 提供 RSA 密钥支持requests 用于接入方侧调用。生产环境还要加数据库和缓存这里先用内存字典把逻辑跑通。3.2 生成 RSA 密钥对OIDC 要求 id_token 用非对称签名认证中心持私钥签发接入方用公钥验签。先生成密钥# keygen.py from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization # 生成 2048 位 RSA 私钥 private_key rsa.generate_private_key(public_exponent65537, key_size2048) # 导出私钥 PEM with open(private.pem, wb) as f: f.write(private_key.private_bytes( encodingserialization.Encoding.PEM, formatserialization.PrivateFormat.PKCS8, encryption_algorithmserialization.NoEncryption() )) # 导出公钥 PEM供接入方验签 public_key private_key.public_key() with open(public.pem, wb) as f: f.write(public_key.public_bytes( encodingserialization.Encoding.PEM, formatserialization.PublicFormat.SubjectPublicKeyInfo ))key_size 选 2048 是当前安全下限1024 已被认为不安全。public_exponent 用 65537 是惯例。私钥文件权限要收紧别提交到代码仓库这是血泪经验。3.3 授权端点与令牌端点认证中心至少要有两个端点/authorize 负责发 code/token 负责用 code 换令牌。# idp.py import time, secrets, jwt from flask import Flask, request, redirect, jsonify app Flask(__name__) PRIVATE_KEY open(private.pem).read() ISSUER http://localhost:5000 # 内存存储code 和已注册客户端生产要换 Redis/数据库 codes {} clients { demo-client: { secret: demo-secret, redirect_uris: [http://localhost:8000/callback] } } app.route(/authorize) def authorize(): client_id request.args.get(client_id) redirect_uri request.args.get(redirect_uri) state request.args.get(state, ) client clients.get(client_id) # 校验 client 和回调地址白名单防开放重定向 if not client or redirect_uri not in client[redirect_uris]: return invalid client, 400 # 生成一次性授权码5 分钟有效 code secrets.token_urlsafe(32) codes[code] {client_id: client_id, exp: time.time() 300} return redirect(f{redirect_uri}?code{code}state{state}) app.route(/token, methods[POST]) def token(): code request.form.get(code) client_id request.form.get(client_id) client_secret request.form.get(client_secret) record codes.pop(code, None) # 用掉即删防重放 if not record or record[exp] time.time(): return jsonify({error: invalid_grant}), 400 if clients[client_id][secret] ! client_secret: return jsonify({error: invalid_client}), 401 now int(time.time()) id_token jwt.encode({ iss: ISSUER, sub: user-1001, aud: client_id, exp: now 3600, iat: now }, PRIVATE_KEY, algorithmRS256) return jsonify({access_token: secrets.token_urlsafe(32), id_token: id_token, token_type: Bearer})逻辑说明/authorize 里先校验 client_id 和 redirect_uri 是否在白名单这一步防的是开放重定向漏洞攻击者伪造 redirect_uri 就能把 code 偷走。code 用 secrets.token_urlsafe 生成密码学安全随机别用 random。/token 里 codes.pop 保证一个 code 只能用一次过期时间设 5 分钟。id_token 用 RS256 签名sub 是用户唯一标识aud 绑定 client_id 防止令牌被别的客户端复用。参数上exp 设 1 小时是常见折中太长风险大太短用户频繁重登。redirect_uris 必须精确匹配不要用前缀匹配否则 http://localhost:8000/callback.evil.com 这类地址能绕过。3.4 暴露公钥的 JWKS 端点接入方需要公钥验签标准做法是暴露一个 JWKS 端点from cryptography.hazmat.primitives import serialization import base64, json app.route(/.well-known/jwks.json) def jwks(): pub serialization.load_pem_public_key(open(public.pem, rb).read()) numbers pub.public_numbers() def b64(n): b n.to_bytes((n.bit_length() 7) // 8, big) return base64.urlsafe_b64encode(b).rstrip(b).decode() return jsonify({keys: [{ kty: RSA, use: sig, alg: RS256, kid: key-1, n: b64(numbers.n), e: b64(numbers.e) }]})n 和 e 是 RSA 公钥的模数和指数base64url 编码时要去掉等号填充这是 JWT 规范要求。kid 用于密钥轮换时标识用哪把钥匙接入方按 kid 选公钥。4. 接入方怎么接会话建立与令牌校验4.1 发起登录与回调处理接入方SP用 Flask 写核心是登录跳转和回调两个路由# sp.py import secrets, jwt, requests from flask import Flask, request, redirect, session from jwt import PyJWKClient app Flask(__name__) app.secret_key sp-session-key IDP http://localhost:5000 CLIENT_ID, CLIENT_SECRET demo-client, demo-secret REDIRECT http://localhost:8000/callback app.route(/login) def login(): state secrets.token_urlsafe(16) session[state] state # 存起来回调时比对 url (f{IDP}/authorize?response_typecodeclient_id{CLIENT_ID} fredirect_uri{REDIRECT}scopeopenidstate{state}) return redirect(url) app.route(/callback) def callback(): # 校验 state防 CSRF if request.args.get(state) ! session.pop(state, None): return state mismatch, 400 code request.args.get(code) resp requests.post(f{IDP}/token, data{ grant_type: authorization_code, code: code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: REDIRECT }).json() # 用 JWKS 验签 id_token jwks PyJWKClient(f{IDP}/.well-known/jwks.json) signing_key jwks.get_signing_key_from_jwt(resp[id_token]) claims jwt.decode(resp[id_token], signing_key.key, algorithms[RS256], audienceCLIENT_ID, issuerIDP) session[user] claims[sub] return f登录成功用户 {claims[sub]}逻辑说明/login 生成随机 state 存进 session拼授权地址跳转。/callback 先比对 state不一致直接拒绝这是防 CSRF 的核心。然后用 code 换令牌注意这一步在服务端做。最后用 PyJWKClient 拉公钥验签jwt.decode 里指定 algorithms 为 RS256这一步很关键——如果不指定算法攻击者可以把 alg 改成 none 或 HS256 用公钥当 HMAC 密钥来伪造令牌这是 JWT 最经典的翻车点。audience 和 issuer 也要校验确保令牌是发给自己的、来自可信认证中心。4.2 会话与登出接入方验完 id_token 后一般建立自己的服务端会话session后续请求读 session 判断登录态而不是每次请求都验 JWT这样性能好。登出分两种本地登出只清自己的 session单点登出要跳认证中心的登出端点认证中心再通知各接入方清会话。毕业设计里本地登出够用单点登出可以提一句作为扩展。注意access_token 用于访问资源接口id_token 用于证明身份两者别混用。id_token 不该拿去调接口。5. 避坑与排查那些让演示当场翻车的细节5.1 回调地址不匹配导致 invalid_client现象点登录后认证中心直接返回 invalid_client 或 redirect_uri mismatch。原因接入方注册的 redirect_uri 和实际请求的不完全一致常见是端口不同、多了或少了一个斜杠、http 和 https 混用。解决把白名单做成精确字符串比对调试时把请求里的 redirect_uri 和注册值都打印出来逐字符对比。5.2 时钟偏移导致令牌校验失败现象id_token 明明没过期接入方却报 TokenExpiredError 或 ImmatureSignatureError。原因认证中心和接入方所在机器时间不同步exp 和 iat 校验对时间敏感。解决两台机器都开 NTP 同步jwt.decode 里可以加 leeway 参数容忍几秒偏移比如 leeway10。5.3 算法混淆攻击现象自己写的校验逻辑被伪造令牌绕过。原因jwt.decode 没指定 algorithms攻击者把 header 的 alg 改成 none 或 HS256用公钥内容当 HMAC 密钥签名。解决decode 时强制 algorithms[RS256]并且只信任 JWKS 里的公钥绝不接受令牌自带的密钥信息。5.4 code 被重复使用现象同一个 code 能换两次令牌。原因换完没删除 code 记录或者删除和校验不是原子操作。解决用 Redis 的原子删除或者像上面代码那样 pop 后立即判断生产环境要保证并发下只有一个请求能拿到记录。5.5 state 丢失导致登录循环现象用户登录后又被弹回登录页反复循环。原因session 里存的 state 在回调时读不到常见是跨域下 Cookie 的 SameSite 策略把 session cookie 拦了或者接入方和认证中心不同域导致 session 不共享。解决确认接入方自己的 session cookie 能正常写入跨域场景下 SameSite 设 Lax 或 None 并配 Secure回调地址和发起地址保持同域。6. 验证与进阶怎么证明这套协议真的立住了协议写完不算完得能证明它是对的。我一般用三步验证。第一步手工走一遍完整流程用浏览器开发者工具看每次跳转的 URL 参数确认 code、state、令牌都符合预期。第二步写自动化测试模拟授权码换取和令牌校验重点测异常分支过期 code、错误 secret、篡改的 id_token、state 不匹配这些用例能覆盖大部分安全边界。第三步用 jwt.io 这类工具解码 id_token肉眼核对 iss、aud、sub、exp 是否符合设计。进阶方向有几个值得做。一是引入 PKCEProof Key for Code Exchange在授权请求里带 code_challenge换令牌时带 code_verifier防止授权码被拦截后滥用前后端分离和移动端强烈建议加。二是令牌刷新用 refresh_token 在 access_token 过期后换新的避免用户频繁重登注意 refresh_token 要绑定客户端并支持吊销。三是密钥轮换JWKS 里放多把公钥用 kid 区分旧密钥保留一段时间再下线避免轮换瞬间所有令牌验签失败。验证项方法期望结果授权码一次性同一 code 换两次第二次返回 invalid_grant签名校验篡改 id_token 一位验签失败受众校验用别的 client_id 的令牌audience 校验失败state 防伪回调传错误 state拒绝并返回 400过期控制等 code 超 5 分钟返回 invalid_grant最后说个我自己的习惯每次改完认证逻辑我一定先把「错误路径」跑一遍而不是只测成功登录。因为成功路径谁都能跑通真正决定这套协议能不能上生产的是那些异常分支——过期、篡改、重放、跨站。把这些边界守住统一身份认证才算真的立住了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑