根据ID来管理分布式session - 新老界面session不一致导致强制登出问题的修复

分享到:

背景

由于历史原因,原先的界面是用vaadin框架来实现。但是这个框架不适合互联网的分布式系统,正在逐步用目前主流的前端框架重写各个模块,把旧的vaadin页面替换掉。在替换过程中,新老界面并存。

vaadin界面的servlet,每次都会判断请求中的session id值,如果在服务器中找不到对应这个id的session,就会重新生成一个。由于session id是在新界面登录的时候生成的,当点击链接从新界面跳转到vaadin界面的时候,vaadin服务会发现没有这个session id,就会重新生成一个新的session。换句话说,新老界面会有各自的session id。当然,分布式系统本来就有这个问题,可以采用分布式session的来解决。

原先开发人员的解决方案是:

  1. 把session信息存入redis缓存,session id作为key
  2. 每次跳转到vaadin界面后,用新生成的session id替换掉旧的session id,同时在redis里面把session信息从旧的key,复制到新的key这边。

但是这种解决方案存在一个问题,主要是由上面解决方案的第2点引起的。由于每次从新界面跳转到vaadin界面都会生成新的session id,如果打开两个浏览器页面,分别跳转到新的界面,那么这个就会导致第一个跳转的那个浏览器页面中的session id被覆盖。vaadin框架是有状态的,它在客户端与服务器端保持一个长连接,并检测session id的有效性。session id的变化,导致长连接的登录信息失效,被弹回到登录界面。从用户体验上来讲,就是被强制登出。

从另外一个角度说,原先开发人员的解决方案是不合理的,它也不是一种标准的分布式session的解决方案。

如何解决

比较合适的解决方案是,vaadin界面不能自己生成session id,而是要复用在新界面登录之后所生成的session id。当vaadin界面有请求的时候,根据请求中的session id查询服务器上是否有对应的session,如果有则返回对应的session;如果没有,则新建一个以该session id为主键的session。换句话说,新老界面都应该要使用相同session id,一旦登录之后,在当前用户这次登录的有效生命周期内,session id保持不变。这样就不会存在上面说的,由于session id的变化而导致被强制登出的问题。

具体实现的关键是用到HttpServletRequesWrapper类,它能够快速提供HttpServletRequest的自定义实现。

                        |----------------------|
                        |  (I) ServletRequest  |
                        |----------------------|
                                   |
                                   |
              -------------------------------------------
              |                                         |
              |                                         |
|--------------------------|             |-----------------------------|
|  (I) HttpServletRequest  |             |  (C) ServletRequestWrapper  |             
|--------------------------|             |-----------------------------|             
              |                                         |
              |                                         |
              -------------------------------------------
                                   |
                                   |
                    |---------------------------------|
                    |  (C) HttpServletRequestWrapper  |
                    |---------------------------------|

如下的代码是从 Tomcat 抽取出来的,展现了HttpServletRequesWrapper类是如何运行的。

 1public class HttpServletRequestWrapper extends ServletRequestWrapper 
 2    implements HttpServletRequest {
 3
 4    public HttpServletRequestWrapper(HttpServletRequest request) {
 5        super(request);
 6    }
 7    
 8    private HttpServletRequest _getHttpServletRequest() {
 9        return (HttpServletRequest) super.getRequest();
10    }
11  
12    public HttpSession getSession(boolean create) {
13     return this._getHttpServletRequest().getSession(create);
14    }
15   
16    public HttpSession getSession() {
17      return this._getHttpServletRequest().getSession();
18    }
19  // 为了保证可读性,其他的方法删减掉了  
20}

我们所需要做的事情,就是重写getSession这个方法,获取Cookie中的JESSIONID,来获取或者新建session。这里由于vaadin框架里面的一些实现,把session所保存的状态放入外部存储不是一个合适的选择,所以就新建了个MySessionContext用于存放,当然还需要额外的代码来清除MySessionContext中过期的session信息。

 1public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
 2
 3
 4    public MyHttpServletRequestWrapper(HttpServletRequest request) {
 5        super(request);
 6    }
 7
 8    @Override
 9    public HttpSession getSession(boolean create) {
10        ...
11
12        // 获取请求中的session id,用于获取已有的session或者新建session
13        String requestedSessionId = getRequestedSessionId();
14
15        if(requestedSessionId != null) {
16            // 根据指定的session id,获取已有的session
17            HttpSession session = MySessionContext.getSession(requestedSessionId);
18            if (session != null) {
19                return session;
20            }
21        }
22
23        if(!create) {
24            return null;
25        }
26
27        // 新建session,用指定的session id作为key保存
28        return MySessionContext.addSession(requestedSessionId, createNewSession());
29    }
30
31    @Override
32    public HttpSession getSession() {
33        return getSession(true);
34    }
35
36    @Override
37    public String getRequestedSessionId() {
38        // 读取Cookie中JSESSIONID的值,为了方便阅读,省略具体实现
39        ...
40    }
41
42    // 新建session
43    private HttpSession createNewSession() {
44        return (HttpServletRequest)this.getRequest().getSession(true);
45    }
46}

通过filter来使用包装类

 1public class MyFilter implements Filter {
 2
 3    /*
 4     * 这个方法创建了我们上文所述的封装请求对象, 然后调用其余的 filter 链。
 5     * 这里,当这个filter后面的应用代码执行时,如果要获得session的话,将
 6     * 会使用我们上面所写的方式来获得
 7     */
 8    protected void doFilterInternal(HttpServletRequest request,
 9            HttpServletResponse response, FilterChain filterChain) 
10            throws ServletException, IOException {
11        
12        MyHttpServletRequestWrapper wrappedRequest =
13          new MyHttpServletRequestWrapper(request);
14
15        filterChain.doFilter(wrappedRequest, strategyResponse);
16    }
17}

在web.xml中启用filter

1<filter>
2   <filter-name>MyFilter</filter-name>
3   <filter-class>net.zengxi.filter.MyFilter</filter-class>
4</filter>
5
6<filter-mapping>
7    <filter-name>MyFilter</filter-name>
8    <url-pattern>/module/*</url-pattern>
9</filter-mapping>

参考资料