统一身份认证服务从零实现:单点登录、JWT与密码安全
发布时间:2026/10/1 14:51:05来源:尧图网络
做毕设选“统一身份认证”这个方向我一开始也以为是老生常谈的增删改查真正动手才发现里面的门道远比想象中多。这个题目看着是Python写的但它解决的问题其实跨了好几个技术栈——JAVA、PHP、小程序、爬虫项目要想共用一个账号体系基本都绕不开它。我花了一个多月把这个服务完整实现了一遍期间踩了不少坑也理清了很多思路这里把整个过程、代码思路、演示录像里的测试流程都拆开讲清楚给正准备拿这个题目做毕设的朋友做个参考。1. 项目背景与定位1.1 统一身份认证到底解决什么问题先说一个最直观的场景你手上有三个系统一个是Spring Boot写的后台管理一个是PHP写的官网还有一个是给爬虫脚本用的内部工具。原来每个系统各搞一套用户名密码用户注册了A系统去B系统还得再注册一次管理员想统计“到底有多少活跃用户”都数不清楚。统一身份认证服务就是把这些系统的登录逻辑全部抽出来放到一个独立的服务里统一管理。这样做的好处很直接用户只需要在一处注册、一处登录就能拿到一个通行凭证拿着这个凭证去访问其他系统其他系统不再自己校验密码而是找认证服务确认“这个人是谁、有没有权限”。这个模式现在有个普及度很高的名字叫单点登录SSO但对于毕设来说不需要把SSO做到企业级那么重核心是把“认证”和“业务系统”解耦证明你理解了分布式环境下会话管理的基本思路。这个题目适合谁如果你学过Python的基础语法、了解Flask或FastAPI的基本用法同时会一点HTTP协议和数据库操作做这个项目就不会觉得吃力。如果你主攻的是JAVA、PHP这类方向也同样可以拿这个题目——主体用Python写答辩时讲清楚认证服务的通用性导师不会觉得你偏题反而会认可你对跨语言场景的理解。1.2 选Python而不是其他语言的理由我选Python做主体首先是开发效率确实高。一个可用的认证服务核心代码量其实不多用FastAPI或者Flask写文件结构清晰代码阅读起来也比Java那一套容易很多。对毕设来说代码量适中反而是个优点——太少了显得工作量不足太多了自己后期维护和写论文都痛苦。其次Python生态里有非常成熟的密码哈希库passlib/bcrypt、令牌生成库python-jose/PyJWT和ORM框架SQLAlchemy这些库拼起来整个认证链路很顺畅几乎不用自己去实现加密算法。相比起来Java做同样的事情需要引入Spring Security或者Shiro配置复杂度高一个级别PHP做起来代码能写得很简单但涉及JWT、中间件等概念时需要手写的部分也多。Python在“能够完整讲清楚原理”和“代码量可控”之间取了平衡。当然这不是说Python就完美。项目里我用了Flask原因是对毕设来说Flask足够轻路由和请求处理逻辑一目了然。FastAPI更现代、自带接口文档但如果不需要异步性能和自动生成OpenAPI文档Flask的学习成本会更低一些。实际选型时可以根据自己熟悉程度来关键是整个服务的架构思路是一致的。2. 系统整体设计与核心流程2.1 认证服务的基本模块划分统一身份认证服务拆开来看大致包含这几块用户管理、认证核心、令牌管理、接入方管理、审计日志。用户管理处理注册、密码修改、个人信息维护认证核心处理登录请求、校验密码、生成令牌令牌管理负责令牌的签发、刷新、吊销接入方管理解决“哪些系统可以调用认证服务”的问题审计日志则把每一次登录尝试、令牌刷新等行为记录下来方便追溯。不要一上来就想着把功能堆满。我第一版设计时列了十几个表后来砍到只剩用户表、接入方表、令牌表、审计日志表四张核心表。为什么砍因为毕设答辩时导师更关注你有没有把核心链路想明白而不是你的表设计得有多复杂。多余的功能如果只是“为了有而有”反而会在追问细节时露怯。表少但链路完整每个表都有不可替代的作用这样的设计讲起来最顺畅。2.2 登录与令牌校验的完整流程整个服务的核心是两条链路登录签发令牌链路、业务系统校验令牌链路。第一条链路用户带用户名和密码请求认证服务的登录接口服务端校验通过后生成一个短期有效的Access Token和一个长期有效的Refresh Token把用户ID、角色等关键信息放进Token里返回给前端。第二条链路业务系统收到来自客户端的请求后取出Token拿去认证服务的校验接口验证签名和有效期验证通过再放行业务请求。这两条链路看起来简单但设计时有一个关键取舍业务系统到底应该“拿着Token去认证服务问一遍”还是“自己验签就完事”前者叫远程校验优点是即使Token被篡改也能立刻发现缺点是高并发下认证服务会成为性能瓶颈后者叫本地校验业务系统只需要保存认证服务的公钥自己验签名性能好但Token一旦签发在有效期内即使账号被禁用也无法立刻生效——要等Token过期。毕设里我建议做远程校验。原因很朴素实现简单、逻辑清晰、答辩时好解释。你只要在认证服务里提供一个校验接口业务系统那边通过HTTP调用就行。本地校验涉及密钥分发和缓存失效策略复杂度上去了但如果论文里想多写一个亮点可以在“后续优化方向”里提一句“支持JWT本地验签模式”点到为止就够了。2.3 数据表结构设计思路用户表的核心字段是用户名、密码哈希、盐值如果哈希方案需要、邮箱、手机号、状态、创建时间。这里特别要注意明文密码绝对不存哈希必须是带随机盐的慢哈希算法。接入方表存各个业务系统的标识client_id、密钥client_secret、回调地址如果需要OAuth2的授权码流程。令牌表存Access Token和Refresh Token的哈希值、关联用户ID、接入方ID、过期时间、吊销状态。审计日志表记录时间、用户、IP、动作、结果。我当时设计时纠结过一个问题要不要存Token原文结论是不要。数据库一旦泄露Token原文等于把用户的登录态直接送给攻击者。正确做法是只存Token的SHA-256哈希值校验时先对请求里的Token做同样的哈希运算再比对。这个细节在论文里写出来是一个不错的加分项属于“教科书上看得到、但很多源码里没做”的安全意识。存储Refresh Token时还需要考虑一点Refresh Token的生命周期比Access Token长很多如果它被泄漏了攻击者可以长时间保持登录状态。于是我用了一个简单的方案Refresh Token在数据库里有记录每次刷新时把旧的Revoke掉发一个新的这样即使Refresh Token被截获也只能用一次。代价是每次刷新都多一次数据库写操作但对毕设的访问量来说毫无压力。3. 核心实现细节与关键代码解析3.1 用户注册与密码安全处理密码安全是认证服务的命门。我在项目里没有用MD5或者SHA-256直接加密——这两种算法虽然常见但速度快暴力破解的成本低。我用的方案是bcrypt它自带盐和代价因子每次哈希结果都不同而且可以通过调整代价因子让计算变慢对抗暴力破解。Python里用passlib库封装得很好几行代码就搞定from passlib.hash import bcrypt # 注册时哈希密码存储 password_hash bcrypt.hash(user_password) # 登录时校验密码 is_valid bcrypt.verify(input_password, password_hash)这里有一个实际踩过的坑bcrypt对密码长度是有限制的早期版本只处理前72个字节超过部分会被静默丢弃。用户如果注册了一个特别长的密码之后登录时输入同样长的密码也能通过因为被截断后哈希结果一致但如果用户后面改了密码中间某几位就会出现“密码明明一样却校验失败”的诡异问题。解决方法是注册时校验密码长度超过64个字符就提示用户修改或者先做一次SHA-256再交给bcrypt处理。另一个坑是passlib和bcrypt版本兼容问题。我当时装环境时passlib 1.7.4和bcrypt 4.x搭配使用调用bcrypt.hash时直接抛异常原因是新版bcrypt移除了对passlib里某个内部调用的支持。解决方法是把bcrypt锁到3.2.x版本或者干脆直接用bcrypt库自己的接口写一个封装。博客和教程里经常用的passlib写法在新环境下不一定能跑通遇到问题先看版本。3.2 基于JWT的令牌签发与校验JWT在整个项目里占据核心位置。我用PyJWT签发TokenAccess Token的有效期设为30分钟Refresh Token设为7天。JWT的标准结构包含Header、Payload、Signature三部分Header声明算法Payload放用户ID、角色、过期时间Signature用服务端的密钥对前两部分签名。签发令牌的代码大致长这样import jwt from datetime import datetime, timedelta SECRET_KEY your-secret-key # 实际应用必须用环境变量注入不能硬编码 def create_access_token(user_id, role): payload { sub: str(user_id), role: role, exp: datetime.utcnow() timedelta(minutes30), type: access } return jwt.encode(payload, SECRET_KEY, algorithmHS256)校验令牌时只需要解码并验证签名和有效期def verify_token(token): try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) return payload except jwt.ExpiredSignatureError: return None # Token过期 except jwt.InvalidTokenError: return None # 签名错误或格式错误需要注意Payload里的信息是Base64编码的不是加密的。也就是说别人可以轻松解码看到你在Token里放了什么内容所以任何敏感信息密码哈希、手机号、身份证都不要往JWT里塞。JWT只适合放“不敏感但业务需要”的信息比如用户ID、角色编码。如果要做注销功能也不能只靠JWT本身必须配合服务端的令牌黑名单或者数据库标记来实现。我在项目里选择了“数据库标记短有效期”的组合Access Token有效期短泄露窗口小Refresh Token存在服务端数据库里吊销时直接删记录。3.3 单点登录的简化实现完整的SSO协议比如CAS或OAuth2对毕设来说过于复杂但可以在认证服务里用一套简化的方案来描述清楚用户登录认证服务后拿到Token访问接入系统时带上Token接入系统把Token送到认证服务校验校验通过就认为用户已登录并在本地建立会话。这套方案我在演示录像里专门录了一段开两个浏览器窗口一个访问认证服务登录页另一个直接访问接入系统的受保护资源携带Token后可以正常访问不携带则被重定向到认证服务登录页。整个过程对用户来说就是“一次登录到处访问”从演示效果上看已经具备了单点登录的核心体验。如果说得再深入一点企业里做SSO还要考虑“登录态如何在多个域名之间共享”的问题这往往需要借助Cookie的Domain属性或者前端传Token。毕设阶段不用把这块做得太复杂接口设计时预留一个client_id参数表示“当前是哪个接入系统在请求认证”就足以体现你对多系统接入的理解了。3.4 防暴力破解与会话保护登录接口如果没有防暴力破解机制等于把密码字典直接暴露在公网上。我实现了一套基于IP和用户名的双维度失败次数限制同一个IP 10分钟内失败超过20次锁定该IP 30分钟同一个用户名15分钟内失败超过10次锁定该用户名30分钟。锁定的状态我放在Redis里用EXPIRE自动过期避免维护一个无限增长的锁定表。如果环境里没有Redis也可以用内存字典加时间戳来实现但重启即失效。我建议毕设至少用一个Docker起的Redis容器这不仅能让代码更接近生产实践论文里还能写“缓存层用于支撑高频读写场景”显得技术选型有考量。另外一个小细节登录成功之后服务端应该重新生成会话标识而不是沿用登录前的标识。因为登录前可能通过URL参数或者Referer泄漏过标识沿用旧标识存在“会话固定攻击”的风险。这个细节很多教程不会讲但答辩时一旦被问到答上来就是明显的加分项。4. 环境准备与实操复现4.1 本地开发环境搭建整个项目我用的是Python 3.10版本依赖的核心库是Flask、PyJWT、bcrypt、SQLAlchemy、Redis。虚拟环境我用venv管理避免和系统Python的包冲突。安装依赖我用了一个requirements.txt内容大致如下Flask2.3.3 PyJWT2.8.0 bcrypt4.0.1 SQLAlchemy2.0.23 redis5.0.1 requests2.31.0数据库我用的SQLite因为毕设阶段不想让环境配置成为门槛。SQLite是文件型数据库零配置文件适合演示。但如果数据量增长明显后期要切到MySQL其实也容易SQLAlchemy ORM已经把数据库差异抹平了只需要改连接串。这里有一点要说明bcrypt这个库我最终是用它自己的接口而不是passlib因为passlib的新版本适配问题比较折腾直接操作bcrypt反而更直观。初始化数据库和Redis的步骤如下先启动Redis容器再初始化数据库表结构最后启动Flask服务。如果你不想用Docker直接本地安装Redis也可以Windows下下载Redis的MSI包安装后默认端口6379基本不用改配置。启动服务后我习惯先做一轮接口自测注册新用户、重复注册看是否报错、错误密码登录看返回信息、正确密码登录看Token是否生成。这一轮自测跑通之后再开接入方模拟脚本整个链路才算验证完。4.2 目录结构与核心代码模块项目目录结构建议这样组织auth-service/ ├── app.py # 服务入口路由注册 ├── config.py # 配置项密钥、过期时间、锁定阈值 ├── models.py # SQLAlchemy模型定义 ├── auth/ │ ├── token.py # JWT的签发和校验 │ ├── password.py # 密码哈希与校验 │ └── limiter.py # 登录频率限制 ├── api/ │ ├── register.py # 注册接口 │ ├── login.py # 登录接口 │ ├── refresh.py # 刷新令牌接口 │ └── verify.py # 令牌校验接口 ├── utils/ │ └── response.py # 统一返回格式 └── tests/ └── test_auth.py # 接口测试脚本这个结构的核心思路是app.py只负责启动和路由注册业务逻辑按模块拆分每个模块只做一件事。答辩时导师如果问“可扩展性怎么保证”你可以直接指着这个目录结构说如果要加“忘记密码”功能只需要在api/下新增一个模块然后在app.py里注册路由不需要改动认证核心代码。4.3 模拟接入方的接入演示为了让演示录像更有说服力我写了一个模拟接入方它本身不存用户密码所有认证动作都甩给认证服务。模拟接入方的逻辑其实就两个接口一个是“跳转登录”未登录时把请求重定向到认证服务的登录页登录成功后携带Token跳回另一个是“校验当前用户”用Token调用认证服务的校验接口拿到用户信息再决定放行或拒绝。模拟接入方我用的是Python的FlaskJAVA或者PHP的接入逻辑也是这个思路。无论接入方技术栈是什么它和认证服务的交互无非就是HTTP请求传什么参数、验什么签名、解析什么返回协议层面讲清楚换语言只是语法不同。这也是为什么题目可以同时标注适合JAVA、PHP、爬虫等方向的原因——认证服务是通用的接入方不限语言。测试时我会故意构造几种异常情况Token过期、Token被篡改手动改最后一个字符、Refresh Token被重复使用。每测完一种看一眼审计日志表里的记录是否对应确认日志模块正常工作。这个过程既验证了代码健壮性又给了论文“测试分析”一章足够的数据支撑。4.4 演示录像内容设计演示录像的时间我控制在15分钟左右太短则讲不清楚太长答辩时观众看烦了。录像的开头先演示项目启动过程启动Redis、启动认证服务、启动两个模拟接入方让看的人知道“这套东西是怎么跑起来的”。然后是功能点演示正常注册登录、错误密码提示、Token过期后刷新、模拟接入方无Token访问被拒、多系统单点登录效果。最后是数据库演示登录后查看令牌表变化、账号锁定记录、审计日志完整链路。录像时我踩过一个坑操作系统剪贴板和虚拟机之间的复制粘贴经常失灵导致演示登录时密码输入老出错。后来我直接把默认账号密码写死在演示脚本里减少手动输入的环节整个录像就流畅多了。如果你也做演示录像这个细节值得留意——减少手动操作脚本化演示效果更专业。5. 常见问题与排查技巧5.1 跨域请求导致登录接口被拦截前端如果做一个简单的登录页面调用认证服务的接口最容易遇到的是跨域问题。浏览器请求头带Origin服务端如果没配置Access-Control-Allow-Origin浏览器直接拦截响应前端看到的报错是“CORS policy: No Access-Control-Allow-Origin header is present”。这个报错不是后端接口挂了而是浏览器安全策略拦住了。解决方案是在Flask里启用flask-cors库配置允许的来源列表。开发环境可以放得宽一点但生产环境务必把Origin列表锁定为接入方的真实域名。这里有一个需要注意的安全点如果为了省事设置Access-Control-Allow-Origin: *意味着任何网站都能从浏览器发起请求调用你的认证接口虽然JWT机制还拦着业务数据但CSRF风险面会明显变大。毕设里至少要把登录接口的跨域来源限制住能体现安全素养。5.2 Token过期了前端还在无限循环请求这个问题很典型前端Axios拦截器拿到401响应后用Refresh Token去换新的Access Token换成功后重放原请求逻辑看起来没问题但代码里如果没加“正在刷新”的状态判断多个接口同时返回401就会并发触发多次Refresh请求前一次刷新把Refresh Token踢下线了后一次刷新就拿着失效的Token在空转导致整个页面一直刷新失败。解决方案是维护一个全局的刷新状态在第一个Refresh请求发出时把后面进来的401请求排队挂起等Refresh完成后再统一重放。这个逻辑在前端不算复杂但确实是一道经典面试题答辩时如果被问到“多接口同时过期怎么处理”能说清楚这个方案导师会认为你有实际项目经验。5.3 bcrypt版本兼容异常前面提到过passlib和bcrypt版本冲突问题这里再补充一个细节如果你在Windows上跑bcrypt可能会遇到需要MSVC编译器的报错。解决方法是直接安装预编译的wheel包或者用Python 3.10以上版本配合bcrypt 4.x多数情况下可以避免编译问题。如果还不行退一步用pbkdf2_sha256代替bcrypt也算是慢哈希算法安全度达标就是“技术亮点”讲起来不如bcrypt有说服力。判断一个哈希方案是否合适的核心指标是“计算耗时”。bcrypt默认12轮耗时大约在100~200毫秒pbkdf2_sha256也可以调到类似的耗时水平。这个耗时纯属故意就是为了让暴力破解的成本大幅上升。你在论文里写性能测试时反而可以把登录接口响应时间相对较慢这个现象拿出来解释说明这是安全性和性能的有意权衡。5.4 接口返回格式不统一导致前端解析困难认证服务对外提供多个接口如果每个接口的返回格式都不一样前端就要写一堆分支判断。我在项目里统一了返回格式{ code: 0, message: ok, data: {} }code表示业务状态码0是成功非0是各种错误message是给前端直接展示的提示data携带具体数据。登录成功时data里放Token和用户信息校验失败时data为空。这个格式一统一前端只需要判断code解析逻辑就收敛了很多。如果接入方是爬虫脚本这种格式也很友好——脚本里只要判断json[code] 0不存在“不同接口字段名不同”的解析负担。统一返回格式看似小事但对接多语言接入方时能省大量沟通成本属于“做过项目的人都会坚持、没做过的容易忽略”的细节。6. 接入方开发经验与多场景适配6.1 爬虫项目如何接入统一认证爬虫方向接入认证服务本质上就是“用账号密码换Token然后带着Token去请求受保护的目标资源”。与浏览器场景最大的区别在于爬虫不能依赖浏览器的Cookie自动管理机制只能自己维护Token并且在Token过期前主动刷新或重新登录。我在一个爬虫脚本里封装了一个带自动刷新的会话类class AuthSession: def __init__(self, auth_url, username, password): self.auth_url auth_url self.username username self.password password self.access_token None self.refresh_token None def login(self): # POST /api/login拿到token ... def get_headers(self): return {Authorization: fBearer {self.access_token}} def request(self, method, url, **kwargs): resp requests.request(method, url, headersself.get_headers(), **kwargs) if resp.status_code 401: self.refresh_token() resp requests.request(method, url, headersself.get_headers(), **kwargs) return resp这个封装的意义在于爬虫的业务逻辑只关心目标URL的请求和解析完全不用管认证细节。而且无论在单线程还是多线程环境下只要把AuthSession设计成线程安全的所有线程都能复用同一套登录态减少了重复登录和账号被锁定风险。6.2 小程序和APP端接入的差异小程序和APP接入认证服务时不能在WebView里走Cookie流程主流做法是把登录态放在本地存储里。小程序用wx.setStorageSyncAPP侧也就是SharedPreferences或UserDefaults请求时手动在Header里带Authorization: Bearer token。这里有一个要注意的点小程序或APP的Token如果被本地存储一旦客户端被root或破解Token可能被导出。缓解手段是请求时绑定设备指纹比如把设备ID哈希后放进Token的Payload里服务端校验时检查请求的设备标识是否匹配。这个方案同样不复杂也适合写进论文作为安全防护手段之一。6.3 JAVA、PHP后端接入时的侧重点JAVA后端接入认证服务很多人在网上看惯了Spring Security的教程容易陷入“要引入整套安全框架”的思维惯性。实际上对于接入统一认证服务这种场景你的服务只是一个“资源服务器”你只需要写一个认证过滤器从请求头取Token调用认证服务的校验接口把用户信息放进上下文然后放行业务接口。Python的Flask、Java的Filter、PHP的中间件其实在做同样的事情拦截、校验、放行。JAVA接入时特别容易踩坑的是RestTemplate或Feign调用认证服务时超时时间设置太短导致高并发下大量校验超时。建议单独给认证服务调用配置连接超时和读取超时比如连接2秒、读取5秒同时加上简单的熔断逻辑——认证服务挂了就快速失败不要在业务线程里无限等待。这些思路放之四海皆准哪个语言接入都用得上。7. 项目测试与答辩准备建议7.1 接口测试的有效方法我用requests写了一个简单的接口测试脚本覆盖注册、登录、错误密码、Token过期、刷新、注销等场景。测试脚本本身也是论文里“功能测试”章节的素材里面会记录每个用例的输入、预期输出和实际结果。这个测试脚本不一定要用pytest哪怕是一个函数式的脚本文本只要逻辑清晰答辩时能现场跑一遍就比纸上谈兵强得多。演示时最推荐的一个场景是故意写错密码三次触发锁定规则然后展示Redis里出现一条锁定记录再用正确密码登录也返回“账号已锁定”。这个画面直观展示了限流逻辑的真实有效比口述“我做了安全防护”有说服力得多。7.2 答辩容易被追问的安全问题“你用什么算法哈希密码为什么不用MD5”这个问题几乎是必问。你要能答出密码哈希和加密的区别加密可以解密哈希不可逆MD5和SHA-256速度快所以不适合存密码bcrypt是慢哈希且自带盐所以适合。如果被追问“盐存在哪里”可以说盐是随机生成并拼在哈希结果里的不需要单独存bcrypt.verify时会自动提取。“JWT被窃取了怎么办”这个问题也有固定套路回答Access Token有效期短把泄露窗口压缩到最小Refresh Token实现了一次性使用即使被截获也不能长期持有另外审计日志记录了每次刷新来源IP异常时可以通过吊销接口把Refresh Token列入黑名单。这套回答闭环完整是加分的。7.3 毕设源码整理的几个值得注意的细节整理源码交付物时务必把config.py里的密钥放到环境变量或者.env文件里提交时把真实密钥替换成占位符。这不只是保护自己的环境信息更重要的是把自己练成一个能体现工程素养的人——很多代码仓库的教训就是密钥硬编码在源码里导致整套服务沦陷。另外requirements.txt里尽量把依赖的版本锁死避免别人复现时因为一个不兼容的版本升级而跑不通。文档部分写清楚启动步骤先启动Redis再初始化数据库再启动Flask服务。如果这些步骤不写源码免费给别人也不一定跑得起来反而容易招抱怨。演示录像建议放在代码仓库的docs/目录里文件名带上日期比如demo-03-18.mp4这样和项目标题的“03-18”对应上也方便你回看对比不同阶段的实现状态。7.4 项目扩展方向与后续升级想法认证服务做完基础版本之后可以向几个方向扩展支持OAuth2授权码模式接入第三方登录比如扫码登录增加用户角色权限管理RBAC用Redis缓存用户信息降低数据库压力。粒度从小到大每做一个扩展论文对应章节都会更充实。我个人做完这个项目之后的一个体会是很多Demo级的认证服务最缺的并不是功能而是“防呆设计”。比如重复用户名注册的提示是否友好、密码重置流程是否有时间限制、审计日志会不会迅速膨胀这些点看起来不起眼但对系统健壮性的影响极大。答辩时与其被追问到某个漏洞不如先把这些基础做扎实。如果你拿到这个题目希望你也能先照这个思路把一个完整闭环跑通再考虑怎么加亮点——一个能稳定复现、逻辑自洽的认证服务本身就是最好的亮点。
网站建设高端定制企业官网