RestTemplate发送请求时Cookie的影响及注意事项说明
作者:毕小宝
背景
一个基于 SpringCloud 的多服务项目中,服务间调用通过 Spring 的 RestTemplate 实现,后台模块 A 有一个定期清理无效业务数据的任务,它调用 Web 服务 B 的 API 时,竟然一直出现 Token 已过期问题。
技术背景
- Web 服务权限认证使用 Token ,登录校验成功后,刷新 Token 并调用 response.addCookie 返回给调用方——浏览器或者内部服务。
- Web 服务的拦截器,它提取 Token 的顺序是:请求参数、Cookie、Header,然后验证 Token 的有效性。校验通过,刷新 Token 到响应对象的 Cookie 中。
- Token 有效期限 30 分钟。
- 后台定时任务模块 A 每小时调用一次模块 B 的的 API 。
对于 Web 服务模块 B 来说,请求来源有用户浏览器和内部服务 A ,Token 校验通过后会刷新并设置响应对象的 Cookie 中,而 Cookie 又会在下次请求时发送给服务器。
各服务稳定运行后,定时任务 A 调用服务 B 的 API,从来都没有成功过。
后台服务调用时 Cookie 失效问题
问题描述
后台服务 A 在调用 B 的 某API 时,虽然会生成新 Token 信息并设置到头域,但是除了应用启动时成功调用了一次,其他周期的调用都出现了 Token 已过期问题。
问题分析
分析整个认证过程,发现在 Web 的拦截器中,如果检测到了内部服务调用,会刷新 Token 信息并返回给 RestTemplate 对象,而且拦截器提取 Token 的顺序是:请求参数、Cookie、Header。
// 请求参数 String token = request.getParameter("t"); // Cookie if (StringUtils.isEmpty(token)) { Cookie[] cs = request.getCookies(); if (cs != null) { for (Cookie c : cs) { if ("t".equals(c.getName())) { token = c.getValue(); break; } } } } //Header if (StringUtils.isEmpty(token)) { token = request.getHeader("t"); }
问题症结
后台模块 A 一小时执行一次,每次调用使用 Spring 托管的 RestTemplate 单例对象发送请求给 B 时,它包含了上次请求收到的 Cookie 信息。
虽然同时生成了 Token 的头域信息,但是在 Web 端优先校验了 Cookie ,因此认证结果始终是无效 Token ,请求被拒绝。
启示录
我从互联网上得到的最好的经验之一,就是永远不要复制和粘贴不是自己编写的代码。
如果你一定要复制,那就照着它逐字输入,逼着自己思考,这些代码实际上是什么意思。
这是昨天看科技周刊印象深刻的一句话,本文的问题虽然不是复制粘贴导致的,但它别人的代码、别人的思想,我没有深刻分析过。
当我临时救火被分派解决这个问题时,简单看了下代码分析如下:
- 后端服务设置 Token 到了头域
- 而 Web 模块只从 Cookie 和请求参数中获取 Token ,没有从 Header 中获取
想当然地以为,只要加上从 Header 中获取就好了,而且阴差阳错的加在了 Cookie 获取的后面,所以问题还是没有解决。
反复加日志,打印各个信息的 Token ,发现解析时用的 Token 跟头域不一样, Token 失效时间也很规律,就是上次定时任务的调用时间。突然意识到了,Web 服务用的 Token 跟我想的不一样,谁把 Token 给改了?
答案是 Cookie,RestTemplate 竟然在发送请求时把上次的 Cookie 给带上了。30分钟的有效期,下一轮定时任务执行时,早就是失效了啊。
RestTemplate 注意事项
用 RestTemplate 进行服务调用时,最好清掉 Cookie 信息,
一来避免本文出现的情况;
二来,它作为Spring 托管的单例,如果访问的是不同系统的 API ,势必会出现 Cookie 混淆、失效的问题!
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。