Django:HttpOnly Cookie存储的SimpleJWT如何续签访问令牌
结合HttpOnly Cookie的令牌续签实现逻辑
你之前在React里用setTimeout做续签的方案,核心是靠前端JS读取令牌值、判断有效期后主动调用刷新接口——但HttpOnly Cookie对JS完全不可见,这套逻辑根本没法跑,续签逻辑必须全部挪到后端的请求流程里处理,前端全程无感知,反而比前端定时器的方案更可靠,不会出现页面切后台后定时器冻结、用户回来时令牌已过期未续签的问题。
你现有中间件的适用性
你写的这个负责把Cookie内的令牌注入Authorization头的中间件,完全可以承载全链路的续签逻辑,甚至不需要额外开发独立的刷新接口。你目前的中间件只实现了令牌提取、头注入的动作,缺了三个核心环节:
- 对提取到的access令牌做有效性校验
- 识别到access过期时,自动读取Cookie里的refresh令牌生成新的有效access
- 将新生成的access以HttpOnly Cookie的形式下发,同时替换当前请求中的旧令牌值,保证本次请求能正常通过权限校验
具体改造步骤
- 提前划定排除范围
把不需要走令牌校验、续签逻辑的接口加入EXCLUDE_FROM_MIDDLEWARE列表,比如登录接口、登出接口、原有的令牌刷新接口、静态资源路径、健康检查接口。如果不排除这些路径,很容易出现无限递归调用,把服务端调用栈打满。
注意:所有续签逻辑要写在__call__方法里,不要放到process_view钩子中——process_view是路由匹配完成、即将执行视图函数时才触发,这个时候再替换令牌、设置响应Cookie已经来不及了,必须在请求进入视图逻辑之前就完成所有校验、续签动作。 - 分层校验access令牌
拿到Cookie里的access值后,用SimpleJWT自带的工具类校验,要区分两类异常场景:- 遇到令牌被篡改、签名不匹配这类安全级错误,直接放行让后续权限校验模块抛403,绝不触发续签逻辑
- 只有识别到
TokenExpired(令牌过期)异常时,才进入续签流程
- 自动完成令牌替换
触发续签后,从Cookie中提取refresh令牌,用SimpleJWT的RefreshToken类实例化,调用access_token属性就能拿到全新的有效access;之后必须完成两个关键动作:- 把新的access值同步更新到当前请求的两个位置:一是直接修改
request.COOKIES['access'],二是重新赋值request.META['HTTP_AUTHORIZATION']为新的Bearer {新access}——不然本次请求还是会用旧的过期令牌走后续校验,依旧会报401 - 等本次请求处理完返回响应时,把新生成的access写入HttpOnly Cookie,所有配置要和你原来存储令牌的参数完全对齐:开启
httponly、生产环境开启secure、设置适配站点的same_site、路径设为根路径、有效期和原access的配置保持一致
- 把新的access值同步更新到当前请求的两个位置:一是直接修改
- 兜底异常处理
如果取到的refresh令牌也过期/无效,就直接在响应中清除access、refresh两个Cookie,返回401状态码,前端收到401统一跳转登录页即可。
改造后的中间件参考代码
from rest_framework_simplejwt.exceptions import TokenError, TokenExpired from rest_framework_simplejwt.tokens import AccessToken, RefreshToken from django.conf import settings class AuthorizationHeaderMiddleware: def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): access_token = request.COOKIES.get('access') refresh_token = request.COOKIES.get('refresh') new_access_token = None # 命中排除路径直接放行 current_view = request.resolver_match.view_name if request.resolver_match else "" if current_view in EXCLUDE_FROM_MIDDLEWARE: return self.get_response(request) # 校验现有access有效性 if access_token: try: AccessToken(access_token) # 校验通过,直接注入请求头 request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}' except TokenExpired: # access过期,尝试用refresh换新 if not refresh_token: return self.get_response(request) try: refresh = RefreshToken(refresh_token) new_access_token = str(refresh.access_token) # 替换当前请求中的令牌,保证本次请求可通过权限校验 request.COOKIES['access'] = new_access_token request.META['HTTP_AUTHORIZATION'] = f'Bearer {new_access_token}' except TokenError: # refresh也无效,放行让后续校验抛401 return self.get_response(request) # 处理响应,同步新的access到Cookie response = self.get_response(request) if new_access_token: response.set_cookie( key='access', value=new_access_token, httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'], path=settings.SIMPLE_JWT['AUTH_COOKIE_PATH'], max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'].total_seconds() ) return response
常见踩坑提醒
- 完全不需要为了续签保留前端定时器调用刷新接口的逻辑,HttpOnly Cookie的内容JS读不到,这类依赖前端触发的请求反而会引入CSRF相关风险,后端中间件自动续签的方案不需要前端介入,还能避免页面休眠导致定时器冻结的问题
- 续签逻辑只响应令牌过期的异常,遇到签名不匹配、令牌被篡改的场景直接拦截即可,不要拿refresh换新,防止恶意请求绕过校验
- 写入新的access Cookie时,一定要核对所有安全配置,如果漏设
httponly=True,之前把令牌存在HttpOnly Cookie的安全优势就完全失效了
内容的提问来源于stack exchange,提问作者forest
相关产品推荐
相关产品推荐

