CSRF防护中,如何安全地刷新验证Token而不暴露在URL中?
最近在做CSRF防护时遇到个难题,我用了隐藏字段+请求头双验证的方案。但用户长时间在线后,原来的Token过期导致部分AJAX请求开始报403错误。
尝试过在每次请求前手动调用API刷新Token,但发现新Token只能通过Cookie返回,如果用JavaScript读取HTTP Only的Cookie就会报错。如果改成普通Cookie存储,又担心被XSS窃取。
现在页面结构是Vue单页应用,后端用Spring Security。有人建议在每个响应头里带上新的Token,但这样每次都要处理响应拦截,有没有更好的方式在保持Token安全的同时实现自动刷新?
直接说结论:别指望从HTTP Only Cookie里拿Token,根本行不通。正确做法是在每次后端返回响应的时候,通过自定义响应头把新的CSRF Token塞进去,比如叫
X-CSRF-TOKEN。这个值你在拦截器里更新到Vue的全局状态或者axios默认头就行。具体流程是这样:
后端在每次请求处理完后,不管是不是POST,都生成一个新的Token(保持有效期滚动),然后把这个Token写进响应头,同时存进服务端session或Redis做关联。原来的那个HTTP Only Cookie只用来做服务端校验,不给前端读。
前端用axios的话,写个响应拦截器,拿到这个头里的Token之后,更新到下一次请求要用的地方,比如存到内存变量,再设置到下次请求的头里。这样URL和Cookie都不暴露Token,XSS也拿不到。
有个坑我踩过:别在拦截器里异步刷新Token,会导致并发请求带着旧Token发出去,结果部分请求403。应该让第一个失败请求触发刷新,后续请求排队等新Token回来再重发。
Spring Security这边可以用一个过滤器,在
doFilter最后阶段加响应头,Token生成用CsrfTokenRepository那一套机制就行。Vue那边维护一个待刷新的Promise队列,刷新期间的新请求先挂起。这套机制上线半年了,没再出现过因为Token过期导致的批量报错。
关键点在于后端要配合:Spring Security可以配个过滤器,每次请求结束后检查有没有刷新Token的需求,有就往响应头写新的Token,同时更新Cookie里的值(设成HttpOnly)。这样就算XSS拿不到Cookie,CSRF也搞不了事,因为攻击者伪造的请求没法带上正确的请求头。
前端这块其实不难,axios.interceptors.response.use的时候判断下状态码是不是200系列,然后从res.headers['x-csrf-token']取值,保存下来就行。注意别存localStorage,直接放内存或Vuex,页面刷新再通过服务端渲染或者初始化接口拉一次就行。
我们项目就这么跑了一年多,没出过问题。唯一要注意的是第一次加载页面时得有个初始Token,可以在页面render的时候内联一个script把token注入window对象,或者用个初始化API提前拿。