恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Django REST framework JWT认证实战:双令牌机制与安全加固
首页
资讯中心
/
Django REST framework JWT认证实战:双令牌机制与安全加固
Django REST framework JWT认证实战:双令牌机制与安全加固
发布时间:2026/10/14 1:22:47
1. 为什么要在Django项目里自建Token认证做过前后端分离项目的人多半都经历过这个阶段一开始图省事直接用Django自带的Session认证前端登录后靠Cookie维持状态。本地开发时一切正常等到前端换成移动端App、小程序或者后端要同时服务多个客户端时Session那套就开始处处别扭——跨域携带Cookie麻烦、多端登录状态难同步、服务端还得存一堆Session记录。这时候把认证方式换成Token基本是绕不开的一步。我这次要聊的就是基于Django Django REST framework JWT这套组合从零搭一套可用的API Token认证机制。核心目标很明确用户用账号密码换到一对Token访问令牌刷新令牌之后所有需要登录的接口都带着访问令牌来请求服务端验证令牌合法性后放行。访问令牌过期了用刷新令牌换新的不用让用户重新登录。这套东西能解决什么问题简单说三点。第一无状态服务端不用存Session令牌本身携带了用户身份信息验证靠签名天然适合分布式部署。第二跨端友好不管是Web、App还是其他客户端只要能在请求头里带Token就能用不依赖Cookie。第三可控性强令牌有明确的过期时间可以主动失效权限粒度也能做得比较细。适合谁来参考如果你已经写过一点Django知道Model、View、URL怎么配但对DRF的认证体系还比较模糊或者用过JWT但没搞清楚里面每个参数到底在干嘛那这篇内容应该能帮到你。我会把选型理由、核心原理、完整实操步骤、参数计算还有我自己踩过的坑都摊开讲尽量让你看完能直接照着搭起来。需要提前说明的是下面涉及的具体项目名、模块名我都会用通用叫法你套到自己项目里时按实际结构调整即可。整套方案基于常见实践补充了细节不同版本之间可能有细微差异以你实际安装的版本为准。2. 认证方案选型与整体设计思路2.1 为什么是JWT而不是普通Token在DRF里做Token认证其实有两条主流路线。一条是DRF自带的TokenAuthentication用户登录后服务端生成一个随机字符串存进数据库每次请求拿这个字符串来比对。另一条就是JWT令牌本身是一段经过签名的结构化数据服务端不存。我选JWT主要基于几个考量。普通Token每次验证都要查一次数据库请求量大的时候数据库压力不小JWT验证只需要用密钥验签纯计算不碰数据库性能上更占优。另外普通Token默认没有过期机制得自己额外做清理逻辑而JWT天生带exp过期时间。还有一点JWT的Payload里可以塞一些非敏感的自定义信息比如用户ID、角色标识省去一次查询用户的开销。当然JWT也不是没缺点。最典型的就是令牌一旦签发在过期前很难主动作废因为它不存服务端。这个后面我会讲怎么用刷新令牌黑名单机制来缓解。选型这事没有银弹关键看你的场景更在意什么。对大多数中小规模的前后端分离项目JWT的收益是明显大于成本的。2.2 双令牌机制的设计逻辑只用一个访问令牌行不行行但体验不好。访问令牌如果设得太短用户动不动就要重新登录设得太长一旦泄露风险窗口就很大。所以业界常见做法是拆成两个令牌访问令牌Access Token有效期短一般几分钟到几小时用来访问业务接口。刷新令牌Refresh Token有效期长一般几天到几周唯一用途是换取新的访问令牌。这样即使访问令牌泄露攻击者能利用的时间窗口也很短。而刷新令牌虽然有效期长但它只在刷新接口使用暴露面小还可以配合黑名单做主动失效。我一般会把访问令牌设成30分钟到2小时刷新令牌设成7天到30天具体看业务对安全性的要求。金融类场景可以更短内部工具类可以更长。2.3 整体请求流程把流程捋一遍后面写代码时心里就有谱了用户提交账号密码到登录接口。服务端校验通过签发一对令牌返回。客户端保存令牌Web端一般存内存或localStorageApp端存安全存储。之后每次请求业务接口在请求头Authorization: Bearer access_token里带上访问令牌。服务端验证访问令牌的签名和有效期通过则处理请求。访问令牌过期客户端拿刷新令牌去刷新接口换新令牌。刷新令牌也过期或失效用户重新登录。这个流程里第5步的验证是最高频的所以它必须足够快这也是JWT的优势所在。3. 环境搭建与依赖配置3.1 依赖安装与版本说明先把需要的包装上。核心就三个Django本体、DRF、以及处理JWT的库。JWT这块我用的是djangorestframework-simplejwt它是目前DRF生态里维护得比较活跃、文档也比较全的一个实现。pip install django djangorestframework djangorestframework-simplejwt装完之后建议pip freeze看一下版本因为simplejwt在不同大版本之间配置项有过调整。我写这篇时参考的是较新的稳定版本如果你装的是老版本个别配置名可能对不上遇到报错先去官方文档核对一下。3.2 settings.py关键配置依赖装好接下来在settings.py里注册应用并配置认证方式。INSTALLED_APPS [ # ... 其他应用 rest_framework, rest_framework_simplejwt, accounts, # 你自己的用户应用 ] REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), DEFAULT_PERMISSION_CLASSES: ( rest_framework.permissions.IsAuthenticated, ), }这里有个细节值得说。DEFAULT_PERMISSION_CLASSES设成IsAuthenticated意味着全局默认所有接口都需要登录。这是个安全优先的默认值避免你哪天新加了个接口忘了加权限控制结果裸奔。对于登录、注册这类本来就要匿名访问的接口单独用permission_classes([AllowAny])放开就行。反过来如果你默认设成AllowAny那就得给每个需要保护的接口手动加IsAuthenticated漏一个就是漏洞。两害相权我强烈建议用前者。3.3 JWT的详细参数配置simplejwt的配置都放在SIMPLE_JWT字典里。下面这份是我常用的配置每一项我都标了为什么这么设。from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(minutes60), REFRESH_TOKEN_LIFETIME: timedelta(days7), ROTATE_REFRESH_TOKENS: True, BLACKLIST_AFTER_ROTATION: True, UPDATE_LAST_LOGIN: True, ALGORITHM: HS256, SIGNING_KEY: SECRET_KEY, AUTH_HEADER_TYPES: (Bearer,), USER_ID_FIELD: id, USER_ID_CLAIM: user_id, }逐条解释一下。ACCESS_TOKEN_LIFETIME设60分钟是安全性和体验的折中太短用户频繁刷新太长风险窗口大。ROTATE_REFRESH_TOKENS设为True意思是每次用刷新令牌换新访问令牌时连刷新令牌也一起换新的这样刷新令牌相当于一次性的被截获后重放的机会更小。BLACKLIST_AFTER_ROTATION配合前者把用过的旧刷新令牌加入黑名单防止它被重复使用。ALGORITHM用HS256也就是对称加密签名和验证用同一个密钥。对单体或中小规模项目足够了。如果你有多个服务需要独立验证令牌可以考虑RS256非对称加密公钥验证私钥签名但配置会复杂一些。注意SIGNING_KEY这里直接用了Django的SECRET_KEY方便是方便但生产环境务必确保SECRET_KEY是从环境变量读取的绝不能硬编码在代码里提交到仓库。密钥一旦泄露任何人都能伪造令牌。3.4 黑名单应用的启用上面配置里用到了黑名单功能需要额外启用它否则BLACKLIST_AFTER_ROTATION不生效。INSTALLED_APPS [ # ... rest_framework_simplejwt.token_blacklist, ]启用后要执行迁移它会创建两张表分别记录未过期的令牌和黑名单令牌。python manage.py migrate这两张表会随着令牌签发和刷新不断增长所以定期清理过期记录是必须的运维动作。simplejwt自带了一个清理命令可以挂到定时任务里每天跑一次python manage.py flushexpiredtokens我见过有项目上线几个月没清理这两张表涨到几百万行刷新接口越来越慢最后排查半天才发现是这里的问题。这个坑提前避开。4. 核心接口的实操实现4.1 登录接口签发令牌simplejwt已经内置了登录视图TokenObtainPairView直接挂到URL上就能用。from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, TokenVerifyView, ) urlpatterns [ path(api/token/, TokenObtainPairView.as_view(), nametoken_obtain_pair), path(api/token/refresh/, TokenRefreshView.as_view(), nametoken_refresh), path(api/token/verify/, TokenVerifyView.as_view(), nametoken_verify), ]请求POST /api/token/body里带上username和password返回体里就有access和refresh两个字段。开箱即用非常省事。但实际项目里登录往往不只是返回令牌那么简单。你可能还想返回用户昵称、头像、角色这些信息免得前端登录后再发一次请求查用户资料。这时候就需要自定义序列化器继承TokenObtainPairSerializer并往返回数据里加字段。from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): classmethod def get_token(cls, user): token super().get_token(user) # 往令牌payload里塞自定义信息 token[username] user.username token[role] user.role return token def validate(self, attrs): data super().validate(attrs) # 往返回体里加用户信息 data[user] { id: self.user.id, username: self.user.username, role: self.user.role, } return data然后在视图里指定这个序列化器class CustomTokenObtainPairView(TokenObtainPairView): serializer_class CustomTokenObtainPairSerializer这里有个经验点往令牌payload里塞的信息要克制。JWT的payload是Base64编码的不是加密的任何人拿到令牌都能解出里面的内容。所以只放非敏感的标识信息像用户ID、角色这种。手机号、邮箱、身份证这类坚决不要往里塞。而且payload越大令牌字符串越长每个请求都要传输白白增加带宽。4.2 刷新接口换取新令牌刷新接口同样用内置的TokenRefreshView。请求POST /api/token/refresh/body里带refresh字段返回新的access。因为我们开了ROTATE_REFRESH_TOKENS返回体里还会带一个新的refresh客户端记得把旧的替换掉。这里要提醒一个容易出错的点。开了轮换之后旧的刷新令牌会立即失效。如果客户端在并发场景下同时发起多个刷新请求只有一个能成功其他的会拿到401。前端要做个处理刷新请求加锁或者收到401后统一走一次刷新再重试。这个坑在移动端弱网环境下特别容易触发我当初排查了好久才定位到。4.3 受保护接口的写法有了认证机制写受保护接口就很清爽了。DRF的认证类会自动从请求头里解析令牌验证通过后把用户对象挂到request.user上。from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated class ProfileView(APIView): permission_classes [IsAuthenticated] def get(self, request): user request.user return Response({ id: user.id, username: user.username, email: user.email, })客户端请求时带上头Authorization: Bearer access_token就能拿到当前用户信息。如果令牌无效或过期DRF会自动返回401前端捕获到401就走刷新流程。4.4 自定义权限类IsAuthenticated只判断有没有登录实际业务里经常需要更细的权限比如只有管理员能访问。这时候自定义权限类就派上用场。from rest_framework.permissions import BasePermission class IsAdminRole(BasePermission): message 需要管理员权限 def has_permission(self, request, view): return bool( request.user and request.user.is_authenticated and request.user.role admin )用的时候挂到视图上即可。message属性会在权限不通过时作为错误信息返回比默认的英文提示友好得多。我建议每个自定义权限类都写上message前端展示错误时能直接用。5. 令牌安全加固与进阶处理5.1 令牌存储位置的选择令牌签发出来客户端存哪里这是个安全性和便利性的权衡题。Web端常见三种选择存储位置优点缺点适用场景localStorage使用方便跨标签页共享易受XSS攻击读取对安全要求不高的内部系统sessionStorage标签页关闭即清除同样受XSS影响不跨标签短会话场景HttpOnly CookieJS无法读取防XSS需处理CSRF跨域配置复杂安全要求高的场景我的建议是如果项目对安全要求高访问令牌放内存页面刷新就没了靠刷新令牌恢复刷新令牌放HttpOnly Cookie。这样即使有XSS攻击者也拿不到长期有效的刷新令牌。如果只是内部工具localStorage图省事也能接受但要清楚风险在哪。5.2 主动让令牌失效JWT最大的软肋就是签发后难以主动作废。用户改了密码、账号被封禁理论上旧令牌在过期前还能用。simplejwt的黑名单机制能解决一部分问题——把刷新令牌加入黑名单它就换不出新访问令牌了。但已经签发的访问令牌在它过期前仍然有效。要彻底解决有两个思路。一是把访问令牌有效期设得很短比如5分钟让失效窗口足够小。二是在服务端维护一个令牌版本号用户改密码时版本号加一验证时比对令牌里的版本号和当前版本号是否一致。后者需要自定义认证类实现起来稍复杂但对安全性要求高的场景值得做。from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.exceptions import InvalidToken class VersionedJWTAuthentication(JWTAuthentication): def get_user(self, validated_token): user super().get_user(validated_token) token_version validated_token.get(token_version) if token_version ! user.token_version: raise InvalidToken(令牌已失效请重新登录) return user用户模型上加一个token_version字段签发令牌时写进去改密码时递增。这样旧令牌立刻失效。这个方案我在几个对安全敏感的项目里用过效果不错。5.3 限流防暴力破解登录接口是暴力破解的重灾区必须加限流。DRF自带限流机制配置一下就能用。REST_FRAMEWORK { # ... DEFAULT_THROTTLE_CLASSES: [ rest_framework.throttling.AnonRateThrottle, ], DEFAULT_THROTTLE_RATES: { anon: 20/min, }, }匿名用户每分钟20次对正常用户够用对暴力破解就是一道墙。登录接口可以单独设更严格的限流比如每分钟5次。配合账号锁定策略连续失败N次锁定一段时间防护就比较到位了。注意限流基于IP或用户标识如果你的服务前面有反向代理要确保能拿到真实客户端IP否则所有请求会被当成同一个来源正常用户也会被误伤。6. 常见问题排查与避坑实录6.1 令牌验证失败的排查思路令牌验证失败是最常见的问题报错信息往往比较笼统。我整理了一张速查表按现象定位原因。现象可能原因排查方向401 且提示令牌无效签名密钥不一致检查多环境SECRET_KEY是否统一401 且提示令牌过期访问令牌超时走刷新流程或调整有效期401 且提示认证头格式错误请求头写法不对确认是Bearer token格式刷新时提示令牌在黑名单旧刷新令牌被重复使用检查前端是否并发刷新令牌能解出但用户查不到用户已被删除检查USER_ID_CLAIM对应字段签名密钥不一致这个坑特别隐蔽。开发环境用一套密钥测试环境用另一套令牌跨环境自然验不过。如果你们有多环境部署务必确认密钥管理策略别让令牌跨环境混用。6.2 时钟偏移导致的诡异过期JWT的exp是基于签发服务器的时间。如果签发令牌的服务器和验证令牌的服务器系统时间有偏差就可能出现刚签发就过期或者过期了还能用的诡异现象。分布式部署时尤其要注意所有节点必须做时间同步。这个问题排查起来很折磨人因为本地测试永远复现不了只有线上多节点才偶发。我的经验是部署时把NTP时间同步作为标准动作别省这一步。6.3 令牌体积过大的处理前面提过往payload塞太多信息会让令牌变长。我实测过一个极端案例payload里塞了用户权限列表、部门树、菜单配置令牌字符串长到快2KB每个请求都带着它接口响应时间肉眼可见地变慢。后来把非必要信息全砍掉只留用户ID和角色令牌缩到300字节以内问题解决。令牌里只放验证身份必需的最小信息其他数据需要时再查这是原则。6.4 刷新令牌轮换的并发问题前面提过并发刷新会导致部分请求失败。除了前端加锁服务端也可以做点优化。比如给刷新接口加一个短暂的宽限期旧刷新令牌在轮换后的一小段时间内仍然可用。不过这需要改simplejwt的默认行为实现成本不低。更简单的做法是前端统一封装请求库拦截401后暂停其他请求等刷新完成再统一重放。这个方案我在多个项目里用过稳定可靠。7. 我在实际项目中的几点体会整套机制搭下来代码量其实不大simplejwt把大部分脏活累活都干了。真正花时间的是那些边界情况的处理——令牌过期后的用户体验、并发刷新的竞态、多环境密钥管理、黑名单表的清理。这些细节官方文档不会重点讲但恰恰是上线后最容易出问题的地方。我个人最看重的一条经验是认证机制的安全性取决于最薄弱的那个环节。你把JWT配得再严谨如果登录接口不限流、密钥硬编码在代码里、令牌存在localStorage还挡不住XSS那整体安全性还是上不去。所以别只盯着JWT本身把登录、存储、传输、失效这几个环节都过一遍才算真正做扎实。最后分享一个实用小技巧。调试阶段你可以用simplejwt的TokenVerifyView快速验证一个令牌是否有效不用每次都发业务请求。把令牌贴进去返回200就是有效返回401会告诉你具体原因。排查问题时能省不少事。另外令牌的payload可以直接用Base64解码看内容网上有很多在线工具本地用Python一行代码也行import base64 payload token.split(.)[1] # 补齐padding payload * (-len(payload) % 4) print(base64.urlsafe_b64decode(payload))调试时看看payload里到底有什么比对着文档猜要快得多。这个习惯帮我定位过好几次令牌里字段名写错的低级问题。