我们的 App 生产上出了一次比较严重的事故, 许多用户投诉登录后能看到别人的信息, 收到投诉后我们就开始查找问题, 一般这样的问题都是线程安全引起的, 所以查找原因的思路也是按线程安全的思路去查.
业务场景是这样的, 用户登录后, 点击一个页面查看信息, 这个信息显示了别人的信息.
登录交易大致流程如下:
- // 一系列验证
- session.setAttribute("id",id); // 身份证号放入 session
- // 其他操作
查看信息交易的流程如下:
- session = request.getSession();
- if(session != null && session.getAttribute("LoginStatus") == True) {
- String id = session.getAttribute("id");
- Information info = queryInfo(id);
- return info;
- } else {
- return "not login";
- }
通过加日志等方法, 我们确认了是查看信息的时候, 从 session 里拿出来的身份证号是其他人的, 但是到底是在什么时候变化的, 没找到, 因为我们一直顺着线程安全的思路, 找全局变量这样的地方.
另外还发现有个地方可疑, 就是有一个异步的线程, 会验证用户身份证, 并且重新在 session 里放一次.
- public void updateId(HttpServletRequest request) {
- HttpSession session = request.getSession();
- String id = validate();
- session.setAttribute("id",id);
- }
这个函数是在登录主交易调起的线程池处理的, 看上去其实没有多大毛病, 而且也是老的代码, 很久了. 而且我发现了一个规律, 被别人看到信息的用户, 登录交易都触发了使用新设备登录, 因为我们加了一个逻辑, 对换设备登录做了验证, 这样的话需要验证短信, 登录分两步了, 第一步比较快的返回了, 但是异步的更新信息流程还在.
于是我怀疑是不是因为 Servlet 的交易已经返回了, 异步的线程虽然拿到了 HttpServletRequest, 但是这个 request 已经无效了或者被复用了.
我写了下面的代码进行验证, 不停的用 curl 调用. Http 请求很快就返回了, 但是我会把 HttpServletRequest 传给一个线程池, 等 5s 后才会去处理这个 Request, 结果果然是有问题的
结果果然有问题, 大部分情况 new_session 都是 null, 但也出现了不是 null 的情况, 这时候发现 session 不是自己的.
HttpServletRequest 是有生命周期的, 当一个 http 请求过来后, 应用服务器解析报文, 把各种参数放到一个 HttpServletRequest 对象中, 然后传递给 Servlet 的 service 函数, service 函数根据里面的方法调用对应的 doGet/doPost 等方法, 而一旦 service 函数调用结束, HttpServletRequest 的生命周期就结束了, 再这之后你继续使用这个对象, 产生的结果是不确定的.
网上遇到这类问题的人不多, 我专门找了 servlet specification, 其中有一章讲 HttpServletRequest 生命周期的.
从中可以得到如下信息:
(1) 三种情况下 request 有效: service 函数内, doFilter 函数内, startAsync 起的异步线程
(2) 在三种情况之外, 使用 request 会产生不确定的结果 (indeterminate results)
(3) 大部分容器在实现 servlet 的时候, 为了提高性能, 会复用 request 对象, 但这不是规范里必须的
其中提到的 startAsync 是 servlet 3.0 开始有的, 它是为了让一个工作线程可以在做 IO 或类似阻塞线程的操作的时候能干其它的事情, 但是它要求异步线程都结束了, 才会将请求返回给客户端, 本质上还是同步的, 只是并行了. 所以要想异步的处理 Request, 必须使用 servlet 自己的异步机制, 但是这样并不能满足我们的需求, 因为我们就是为了不让主线程等待.
用法示例:
如果使用了这个, 那么客户端需要等待 5s 才能拿到结果.
我又看了 tomcat 的源码, 发现它确实对 Request 做了复用:
虽然问题的原因很简单, 但是产生的后果十分严重. 需要异步处理数据的时候一定要特别小心, 此处如果传 Session 就没问题了, 但是还是要尽量避免.
来源: https://www.cnblogs.com/nxlhero/p/11665099.html