
先说一段我自己的经历。去年做前后端完全分离的项目前端是某单页应用后端接口用的 Django DRF。最开始我图省事直接拿 Django 自带的 session 认证顶上结果联调第一天就被打脸小程序端根本不会像浏览器那样自动带 Cookie每次请求都要手动处理 sessionid前后端域名一分开跨域请求要带凭证CSRF 校验又额外冒出一堆配置。折腾了两天之后我把整套方案切到了 Django JWT 认证底层用 djangorestframework-simplejwt 这个组件来实现花了半天时间把签发、刷新、验签、权限控制全部跑通后面再也没因为认证问题返工过。这篇文章就是那段时间的实战记录适合正在做前后端分离、想在自己的 Django 项目里接入 JWT 认证的开发者也适合纠结 session 和 JWT 到底怎么选型的人。1. 为什么我最终选了 JWTSession 在前后端分离场景下的三个痛点很多刚接触前后端分离的人会觉得Django 自带 session 认证不是也能用吗对同一个域名下、纯浏览器场景、不需要横向扩展session 确实够用。但一旦架构发生变化session 的三个痛点会一个接一个冒出来。1.1 移动端没有 Cookie 的原生通道浏览器处理 Cookie 是自动的。服务端在响应头里 set-cookie浏览器存下来下次请求自动带上。但小程序、原生 App 的 HTTP 客户端不会做这件事。我在项目里遇到的第一道坎就是登录之后App 拿不到 sessionid。解决办法不是没有你可以手动把 set-cookie 里的 sessionid 提取出来存到本地再在后续请求中手动塞到请求头里。听起来很直接但实际项目里这样做很别扭每个请求都要从本地存储读取 sessionid 再拼装业务代码被认证逻辑塞满session 有过期时间服务端返回的响应头不一定每次都有 Set-Cookie过期之后客户端根本不知道该什么时候重新登录不同平台对 Cookie 的处理方式还不一样iOS、Android、小程序各写一套兼容代码维护成本直接翻倍。JWT 不存在这个问题它把认证信息直接放进请求头的Authorization: Bearer token里客户端只需要在登录后保存 token 字符串然后在每个请求里统一塞进头部即可。业务代码完全不用关心 Cookie 的存取细节。1.2 服务端横向扩展时的 Session 同步问题Django 默认把 Session 存在数据库表django_session里单机部署没有任何问题。但流量上来之后你大概率会把服务部署到多台机器上前面挂一层负载均衡。这时候问题出现了用户第一次请求落在 A 机器session 写进 A 机器的数据库第二次请求负载均衡到了 B 机器B 机器查不到这条 session用户就被判定为未登录。你可以用 Redis 做统一的 session 存储让所有节点都去同一个 Redis 读数据。这个方案本身没问题但意味着你的基础设施里要多维护一个高可用组件Redis 一挂整个认证体系跟着挂。如果只是一个小项目运维成本会显得特别重。JWT 是自包含的认证所需的信息用户标识、过期时间、签名都在 token 本身完全不依赖服务端存储。A 机器签发的 tokenB 机器拿着同一个密钥就能验签天然支持横向扩展不需要额外引入 session 存储中间件。这一点在微服务和容器化部署的场景下尤其省事。1.3 跨域请求下 CSRF 处理的复杂度Django 的 CSRF 防护机制默认依赖 Cookie。浏览器会为每个来源维护 CookieCSRF 校验通过校验请求中的csrfmiddlewaretoken或者X-CSRFToken头来确认请求来自可信来源。前后端同源的时候这套机制用起来很顺手一旦前端域名和后端 API 域名不一致CSRF 的配置就开始变得繁琐。你需要在请求里带一个可用的 CSRF token还需要处理 Django 对 HttpOrigin 的校验。开发环境用一个端口、部署环境又换一个端口经常出现登录成功了但很多操作报 403的玄学问题。App 和小程序没有浏览器的 Cookie 同源策略概念CSRF 的存在几乎只剩副作用。切到 JWT 之后认证信息全部放在自定义请求头里服务端不再依赖 Cookie 和 CSRF token 的组合来判定这个请求是否来自已登录用户。整个认证链路一下子清爽多了。当然JWT 模式下仍然要考虑 token 被窃取的风险后文会专门讲但对我当时的项目来说开发体验和排查难度确实降了一个量级。2. JWT 认证的底层逻辑三个点分段的 Base64 与签名校验要用好 JWT不能只会配库得先把它到底是个什么东西搞清楚。很多人把 JWT 当成加密的字符串这个理解是错的。2.1 JWT 长什么样JWT 是一段由三个部分组成的字符串用英文句点.分隔形如header.payload.signature实际项目里你会看到像下面这样长长的一串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMwMDAwMDAwLCJ1c2VyX2lkIjoxfQ.abc123signature...为了方便阅读我把 signature 部分缩短了。你不需要把它背下来但得知道每一段是什么。2.2 Header声明算法和类型Header 是一个 JSON 对象经过 Base64Url 编码后成为 JWT 的第一段。常见的 Header 内容长这样{ alg: HS256, typ: JWT }alg表示签名算法typ表示 token 类型。在 Django 项目里如果用 SimpleJWT 默认配置算法就是 HS256也就是 HMAC-SHA256。2.3 Payload承载用户声明和过期时间Payload 是 JWT 的第二段也是一段 Base64Url 编码的 JSON。SimpleJWT 签发的 token 至少会包含下面几个标准声明声明名含义示例值token_typetoken 类型access 或 refreshaccessexp过期时间Unix 时间戳1730000000iat签发时间Unix 时间戳1729999700jtitoken 唯一标识随机的 UUID 字符串user_id当前用户 ID1你也可以在签发 token 的时候往 payload 里塞自定义字段比如用户名、角色等。但这里有个非常重要的原则JWT 的 Payload 只是 Base64Url 编码不是加密。任何人拿到你的 token都可以在浏览器控制台里用atob()或者任意 Base64 解码工具把 payload 解出来。我之前见过有人把用户手机号、身份证号直接塞进 payload理由是需要在前端展示。这就是典型的把 JWT 当成加密容器用早晚出问题。敏感信息必须通过接口查不要放进 token。2.4 Signature保证 token 没有被篡改Signature 是 JWT 的第三段也是整个 token 最核心的安全保障。以 HS256 为例签名是这样算出来的HMAC_SHA256( base64url(header) . base64url(payload), SECRET_KEY )也就是说把前两段拼接起来再用服务端保存的密钥做一次 HMAC 运算得到的结果就是第三段 signature。服务端收到 token 后会用同样的密钥重新计算一遍签名看算出来的结果和 token 携带的签名是否一致。这个流程的意义在于只要SECRET_KEY不泄露任何人都无法伪造一个合法签名的 token哪怕有人把 payload 里的exp改成一年后签名也会因为内容变化而对不上验签直接失败服务端是无状态的不需要去数据库查 session只靠密钥就能判断 token 是否可信。我用一个比喻来理解JWT 好比一张盖了骑缝章的工作证章是真的上面的内容谁都能看但它保证了内容没有被偷偷改过。所以 JWT 解决的是防篡改和可验证不是保密。2.5 服务端如何验证一个 token在 Django 项目里SimpleJWT 已经把验签封装好了但为了理解原理我建议你至少在测试环境里手动解码一次。SimpleJWT 底层依赖的是 PyJWT 库你可以直接用import jwt from django.conf import settings token 这里填你的 access token payload jwt.decode( token, settings.SECRET_KEY, algorithms[HS256] ) print(payload)如果签名不合法或者 token 过期PyJWT 会抛异常。SimpleJWT 的JWTAuthentication做的事情本质上就是这三步拆开 token 拿到 payload、用密钥验签、检查exp和user_id对应的用户是否仍然有效。3. 在 Django 里落地 JWT环境准备与核心配置理解了原理接下来就是真正的动手环节。我按自己当时从零搭建的顺序来写尽量做到每一步都有明确目的。3.1 安装三个核心组件pip install djangorestframework pip install djangorestframework-simplejwt pip install django-cors-headers三个库各司其职djangorestframeworkDjango 的 API 开发框架提供视图、序列化器、权限控制后面所有接口都建立在它之上djangorestframework-simplejwtJWT 的核心实现负责 token 的签发、刷新、校验django-cors-headers处理跨域请求。前后端域名不同这一步跑不掉。需要注意的是djangorestframework-simplejwt会自动依赖PyJWT所以不需要单独再装。装完之后进入项目配置。3.2 新项目务必先配置自定义 User 模型如果你是从零开始建项目强烈建议在第一次执行python manage.py makemigrations之前设置好自定义用户模型。Django 的默认 User 模型在数据库迁移之后很难改动后期想加头像、手机号、性别等字段只能扩展Profile表或做复杂的迁移操作非常痛苦。我当时的做法是创建一个accounts应用python manage.py startapp accounts然后在accounts/models.py里写一个继承AbstractUser的用户模型先不添加任何额外字段纯粹为将来留余地from django.contrib.auth.models import AbstractUser class User(AbstractUser): pass然后在settings.py里告诉 Django 使用这个自定义模型AUTH_USER_MODEL accounts.User这一步必须在首次迁移前完成否则后面改成自定义模型时要清理数据库和迁移记录很折腾。3.3 settings.py 里的 DRF 认证配置接下来配置 DRF 的认证方式。在INSTALLED_APPS和MIDDLEWARE中加入相关组件INSTALLED_APPS [ # Django 自带的 app django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 第三方应用 rest_framework, corsheaders, rest_framework_simplejwt.token_blacklist, # 项目应用 accounts, ] MIDDLEWARE [ django.contrib.sessions.middleware.SessionMiddleware, corsheaders.middleware.CorsMiddleware, django.middleware.common.CommonMiddleware, # 其余的 Django 自带中间件保持不变 ]这里有两个容易踩的细节第一CorsMiddleware的位置要尽量靠前文档里建议放在CommonMiddleware之前否则某些情况下跨域响应头不会正常带上。第二我提前引入了rest_framework_simplejwt.token_blacklist。这个 app 提供 token 黑名单功能用来实现登出和刷新后旧 token 失效后面讲登出那节会用到。如果你现在不想用可以先不加但我建议从一开始就加上省得后面再改。然后是 DRF 的全局认证和权限配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }DEFAULT_PERMISSION_CLASSES设成IsAuthenticated之后所有视图默认都要求登录。如果你有一个接口想让未登录用户也能访问比如注册、获取公开文章列表就在对应视图上加permission_classes([AllowAny])。这个设计思路是默认收紧、按需放开比默认全开更安全适合大多数项目。最后配置跨域允许的来源CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]这里只是一个示例实际项目按你自己前端的域名来填。3.4 SIMPLE_JWT 配置项速查SimpleJWT 提供了一系列配置项常用的我都列在下面from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(minutes15), REFRESH_TOKEN_LIFETIME: timedelta(days7), ROTATE_REFRESH_TOKENS: True, BLACKLIST_AFTER_ROTATION: True, ALGORITHM: HS256, SIGNING_KEY: settings.SECRET_KEY, AUTH_HEADER_TYPES: (Bearer,), AUTH_HEADER_NAME: HTTP_AUTHORIZATION, USER_ID_FIELD: id, USER_ID_CLAIM: user_id, }这些项到底是什么意思配置项作用我的建议值ACCESS_TOKEN_LIFETIMEaccess token 有效期15 分钟REFRESH_TOKEN_LIFETIMErefresh token 有效期7 天ROTATE_REFRESH_TOKENS刷新时是否返回新的 refresh token看需求建议 TrueBLACKLIST_AFTER_ROTATION刷新后是否把旧 refresh 加入黑名单与上一项配合建议 TrueALGORITHM签名算法HS256SIGNING_KEY签名密钥默认用 SECRET_KEYAUTH_HEADER_TYPES请求头中 token 的类型前缀BearerUSER_ID_FIELD用户查询字段id关于过期时间的具体取值我后面专门用一节来讲因为这里面的取舍直接影响用户体验和安全边界。3.5 URL 路由三个内置接口SimpleJWT 直接提供了三个视图对应登录、刷新、验证。在urls.py里加上from django.urls import path 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), ]到这里一个最简可用版本的 JWT 认证其实已经搭起来了。执行python manage.py migrate把相关的表建好再用一个已存在的用户去调用接口就能拿到 token。但实际项目的需求不会这么简单下一节我展开讲完整链路。4. 跑通第一个登录接口签发、携带、校验的完整链路4.1 默认登录接口用户名密码换 token把服务跑起来之后往/api/token/发一个 POST 请求curl -X POST http://127.0.0.1:8000/api/token/ \ -H Content-Type: application/json \ -d {username: testuser, password: yourpassword}成功的话响应里会包含两个字段{ access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... }access是业务请求用的凭证有效期短refresh是刷新凭证用于在 access 过期后换取新 access有效期长。4.2 自定义登录视图把用户信息一起返回实际项目中前端登录成功后不可能只要一个 token用户 ID、用户名、头像这些信息通常是要一并拿到的。虽然前端可以拿 token 去解析 payload但前面讲过 payload 不是加密的直接把用户 ID 放里面没问题头像、昵称这种展示信息也可以通过额外接口查。不过我当时的做法是直接改登录序列化器让登录接口一次性把基础信息返回来减少一次请求。自定义方式如下from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.views import TokenObtainPairView class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): classmethod def get_token(cls, user): token super().get_token(user) token[username] user.username return token def validate(self, attrs): data super().validate(attrs) data[user_id] self.user.id data[username] self.user.username return data class CustomTokenObtainPairView(TokenObtainPairView): serializer_class CustomTokenObtainPairSerializer然后把 urls.py 里的登录视图从TokenObtainPairView换成CustomTokenObtainPairView。这样登录接口的返回会变成{ access: ..., refresh: ..., user_id: 1, username: testuser }get_token这个方法里加的token[username]会写进 payload所以解码 token 的时候也能读到 username。这里我多提一句往 payload 里塞is_superuser这类权限信息要慎重因为 token 签发之后不可变用户一旦被降权旧 token 在过期前仍然有效。4.3 受保护接口怎么接 token拿到 access token 之后前端需要把它放在请求头里curl http://127.0.0.1:8000/api/profile/ \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...DRF 的JWTAuthentication会自动从请求头里找HTTP_AUTHORIZATION截取Bearer之后的部分验签并解析用户最后把用户对象挂到request.user上。我写一个简单的个人信息接口作为示例from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response api_view([GET]) permission_classes([IsAuthenticated]) def profile_view(request): return Response({ user_id: request.user.id, username: request.user.username, })由于全局DEFAULT_PERMISSION_CLASSES已经设成了IsAuthenticated其实不加装饰器也行但显式写出来接口的权限边界一目了然。4.4 手动签发 token 的场景注册接口和后台重置密码默认视图的登录流程是拿用户名密码换 token但有些场景需要手动签发。比如用户注册成功后自动登录比如管理员重置了某用户密码后发一条临时 token 让对方重新验证。手动签发的方式是from rest_framework_simplejwt.tokens import RefreshToken def generate_tokens_for_user(user): refresh RefreshToken.for_user(user) return { refresh: str(refresh), access: str(refresh.access_token), }RefreshToken.for_user(user)是最常用的签发入口。你可以看到它返回的是一个RefreshToken对象对象的.access_token属性拿到的才是 access token。这段代码在注册接口里直接调用即可。4.5 用测试代码验证整条链路我习惯在写业务页面之前先写一段简单的客户端测试确认认证链路是通的。用 DRF 的APIClient就行from rest_framework.test import APIClient def test_token_flow(): client APIClient() # 1. 登录拿 token resp client.post(/api/token/, { username: testuser, password: testpassword, }) assert resp.status_code 200 access_token resp.data[access] # 2. 不带 token 访问受保护接口应该 401 client APIClient() resp client.get(/api/profile/) assert resp.status_code 401 # 3. 带上 token 访问受保护接口应该 200 client.credentials(HTTP_AUTHORIZATIONfBearer {access_token}) resp client.get(/api/profile/) assert resp.status_code 200 assert resp.data[username] testuser这里明确指出了两条容易忽略的信息没有 token 时接口返回 401带 token 时接口才能正常返回。很多入门项目卡住的点其实就在这里。5. 实际项目绕不开的四个问题过期、刷新、登出与跨域配置跑通只是第一步真正在项目里活得久就得处理下面这些看起来不显眼但每天都会遇到的问题。5.1 过期策略access 短一点refresh 长一点JWT 的过期策略是整个认证体系里最需要花心思设计的地方。ACCESS_TOKEN_LIFETIME设得太长比如一天确实省事用户一天之内不用重新登录。但代价是token 一旦泄露攻击者能在一天内畅通无阻地访问该用户的所有权限。所以业界普遍的做法是把 access token 调短减少泄露后的攻击窗口同时把 refresh token 调长避免用户频繁重新登录。refresh token 即便泄露服务端还可以通过黑名单机制把它拉黑比 access token 好处理得多。我常用的组合是场景access tokenrefresh token常规 Web 后台15 分钟7 天移动端 App30 分钟14 天高安全要求的管理系统5 分钟1 天access 过期之后前端用 refresh token 去调/api/token/refresh/拿到新的 access。这一步可以由前端拦截器统一处理用户无感知。5.2 刷新接口的正确打开方式ROTATE_REFRESH_TOKENS 的关键作用刷新接口很简单POST 请求带上 refresh tokencurl -X POST http://127.0.0.1:8000/api/token/refresh/ \ -H Content-Type: application/json \ -d {refresh: 这里填 refresh token}如果ROTATE_REFRESH_TOKENS是 False接口只会返回新的 access tokenrefresh token 不变。如果设成 True接口会同时返回新的 refresh 和 access相当于每次刷新都让 refresh token 也轮换一次。我强烈建议在需要登出功能或者安全要求较高的项目里把ROTATE_REFRESH_TOKENS和BLACKLIST_AFTER_ROTATION都设为 True这样做的逻辑是每次刷新后旧 refresh 进入黑名单无法再次使用即使攻击者拿到了旧的 refresh token也只能用一次且刷新后立刻失效前端保存最新的 refresh token 即可不需要关心历史令牌。这里有一个非常容易踩的坑开启轮换和黑名单之后如果前端因为网络原因把同一个刷新请求发送了多次第二次请求就会收到 401因为旧 refresh 已经被拉黑了。所以在刷新失败时前端要做重定向到登录页的逻辑不要死循环重试。5.3 登出和强制失效黑名单机制怎么用JWT 本身无状态token 签发出去之后服务端默认是不记得它的。要让 token 失效需要额外的手段。SimpleJWT 的 token_blacklist 就是干这个的。登出接口的典型实现前端把要废弃的 refresh token 传到后端后端把它加入黑名单。access token 不需要处理让它自己过期即可。from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken api_view([POST]) permission_classes([AllowAny]) def logout_view(request): refresh_token request.data.get(refresh) if refresh_token: try: token RefreshToken(refresh_token) token.blacklist() except Exception: # token 已经失效或非法直接返回成功即可 pass return Response({message: 已登出})注意登出接口要设成AllowAny因为用户调用它的时候可能 access token 已经过期你不能再要求它带一个有效 token 才能登出。RefreshToken.blacklist()会把这个 token 的 jti 写进黑名单表。之后每次校验 refresh token 时SimpleJWT 都会检查黑名单表发现 token 在黑名单里就拒绝刷新。启用黑名单功能之后记得迁移数据库token_blacklist 相关表是新增的python manage.py migrate5.4 跨域CORS 和 Authorization 请求头的关系前后端分离意味着前后端大概率同域不同端口甚至完全不同的域名。如果只配置了 JWT 但没配置 CORS前端从浏览器发起请求时一样会被浏览器拦截报跨域错误。django-cors-headers的作用是让后端在响应头里返回Access-Control-Allow-Origin等字段浏览器看到这些字段才允许前端代码读取响应。我见过不少人只写了CORS_ALLOW_ALL_ORIGINS True这样开发是方便但生产环境绝不能这么搞等于任何一个网站都可以向你的 API 发请求太危险。正确做法是列白名单CORS_ALLOWED_ORIGINS [ http://localhost:5173, https://app.example.com, ]还有一个容易被忽略的点自定义请求头Authorization默认会被 django-cors-headers 允许但如果你手动配置了CORS_ALLOW_HEADERS一定要把authorization加进去否则浏览器的预检请求会直接 403。from corsheaders.defaults import default_headers CORS_ALLOW_HEADERS [*default_headers, authorization]如果你的应用还需要 Cookie 的场景比如同时保留 session 认证还要加CORS_ALLOW_CREDENTIALS True。但 JWT 场景下我们明确不使用 Cookie所以不要开这个选项开了反而会增加 CSRF 风险。5.5 token 泄露后的应急处理JWT 最被诟病的一点就是难以主动撤销。我自己的处理思路是分层应对第一层保管好签名密钥。SECRET_KEY永远不要提交到代码仓库部署环境用环境变量注入。一旦密钥泄露攻击者可以直接伪造任意用户的 token这是最严重的事故。第二层重视传输安全。生产环境必须强制 HTTPS不能用明文 HTTP 传 token。至于 access token 存 localStorage 还是内存我们当时权衡后选择了存内存页面刷新时通过 refresh 接口重新获取。localStorage 方便但 XSS 漏洞会直接导致 token 被偷这个取舍大家根据自己项目的前端安全水平来定。第三层出事后的止损手段。如果只是某一个 refresh token 泄露调用token.blacklist()把它封掉。如果是大批量用户受影响最彻底的方案是轮换SECRET_KEY但那样会让所有已签发 token 全部失效用户全部要重新登录属于最后的应急手段。还有一个小细节不要在日志里打印请求头尤其不要把Authorization头印出来。我见过有同事为了让前后端联调方便在中间件里把每个请求的头都打到控制台token 就这么留在了日志文件里。6. 我再补充几个容易被文档忽略的实操细节6.1 access 和 refresh 不要混用access token 用于业务请求头refresh token 只用于刷新接口。不要把 refresh token 塞进Authorization头里去请求业务接口也不要拿 access token 去调用刷新接口。SimpleJWT 内部会对 token 的token_type声明做校验混用会直接报错但我在团队里确实见过有人为了省事把 refresh token 当万能 token 用结果接口一直 401 查了半天。6.2 自定义 User 模型时不要硬编码用户类如果你像前面说的那样自定义了 User 模型在序列化器、视图里获取用户时尽量使用get_user_model()from django.contrib.auth import get_user_model User get_user_model()不要在写 token 签发逻辑的时候直接from accounts.models import User一旦将来改了用户模型所有引用点都要跟着改。get_user_model()会动态读取AUTH_USER_MODEL配置减少这类维护成本。如果你的自定义 User 模型改了主键类型比如没用自增 id 而是用了 UUIDSimpleJWT 的USER_ID_FIELD和USER_ID_CLAIM需要相应调整SIMPLE_JWT { USER_ID_FIELD: uuid, USER_ID_CLAIM: user_uuid, }否则默认按 id 查用户会失败。6.3 测试代码里如何快速构造已登录状态写单元测试时每次都先调登录接口再拿 token 有点啰嗦。你可以用RefreshToken.for_user直接构造一个 tokenfrom rest_framework_simplejwt.tokens import RefreshToken from django.contrib.auth import get_user_model User get_user_model() def force_authenticate(client, user): refresh RefreshToken.for_user(user) client.credentials( HTTP_AUTHORIZATIONfBearer {refresh.access_token} )这样在测试用例里一条语句就能模拟登录状态。注意force_authenticate这个命名容易让人误以为是 DRF 自带的force_authenticate我这里故意写成了force_auth避免混淆。6.4 无状态不等于什么都不用存JWT 的无状态指的是服务端不需要保存 session 信息来确认用户身份但如果你想实现登出、强制失效、refresh 轮换就必须引入黑名单表。换句话说完全无状态的应用意味着所有 token 都无法主动撤销只能等过期。这个 trade-off 一定要心里有数。我通常会根据业务安全等级做决策普通 C 端产品access 15 分钟refresh 7 天开轮换和黑名单内部管理系统access 30 分钟refresh 1 天开轮换和黑名单极简 Demo 项目不开启黑名单refresh 一个月token 全量靠过期自然失效。6.5 性能验签开销和数据库查询每次请求JWTAuthentication都会做一次验签HS256 的 HMAC 计算量很小单机 QPS 几千的情况下基本不用担心。但有一个容易被忽略的点JWTAuthentication在拿到user_id之后会去数据库查询用户对象并挂到request.user上。也就是说即使你的接口只返回pong只要用了IsAuthenticated每次都少不了一次用户表查询。在高频接口上如果要优化可以考虑两点在 DRF 的视图里减少对request.user的频繁访问需要时再取值给用户表加一层缓存或者把只依赖 token payload 的数据直接放进 token。但后者要注意 payload 明文可见只放非敏感字段。6.6 最后的最后记得隐藏认证接口的痕迹还有个容易被忽视的点登录、刷新、验证这三个接口默认路径是/api/token/、/api/token/refresh/、/api/token/verify/太规律了。生产环境如果不想让攻击者一眼猜出认证入口可以在路由上加一层前缀比如/api/auth/login/、/api/auth/refresh/配合urls.py改写指向已有的视图。这不是安全强度的关键但能减少一些无意义扫描流量。Django JWT 认证这件事技术本身并没有多复杂真正有讲究的是过期策略、登出方案、泄露应对和 token 轮换这些外围工程。把这一整套链路想清楚并落地你的前后端分离项目在认证这块才算真正站稳了。