记录一次session问题

背景描述:

用户具有多个身份,比如一个人同时兼任技术经理又兼任项目经理,登录时会有多个身份.具体操作就是用户点击登录后 ,弹框选择身份, 不同身份具有不同的权限.
系统是采用两台服务做得集群,服务器是weblogic,集群是F5硬负载.这还是当时负责集群的人留下的坑.

问题

用户反映,当前是A身份登录,操作一段时间按F5刷新,之后变成B身份,问题非常奇怪,
测试难以重现.

可能原因

session有问题,用户操作有问题.

分析过程

1.尝试重现,未重现.

2.询问用户操作时间. 得到如下信息,
这里写图片描述
上图可以看到14:11分用户A身份在线,
这里写图片描述
上图可以看到 14:30 在线 ,A身份.在线
这里写图片描述
上图看出 权限错误, 根据请求可以看出,userid=660001005对应的是身份的编号, 查数据库看出对应的身份是B身份 .并且是数据库中第一条记录. (这里就是坑)

3.找到相应的日志. 分析日志

分析发现:用户14点11分登陆一次,14点30登陆一次. 怀疑用户登录错了身份?
查看用户截图,看图2和图3发现确实存在一个请求发的是A身份,第二个请求发的是B身份的情况.所有排除用户操作问题.

4.继续分析
用户身份是存储到session中的,在session中有个user对象中的. 查看系统中是如何存储身份的.大体伪代码如下:
这里写图片描述
User 中存储了 操作身份的集合,和操作人员的索引,根据索引获取操作人身份.

5.进步一查看springsecurity 模块,涉及用户登录和身份验证的 代码,发现登录逻辑是这样的.
用户点击登录,查询库中所有身份,存储到user对象中,存储到了集合中,然后登陆验证成功后,根据用户弹框选择的身份,给索引号赋值.

$.post("./j_spring_security_check", param).always(
                    function(responseText) {
    
    
                        var json = responseText;
                        if (json.success) {
                            // 设置Cookie永久有效

                                if(!json.czrys || json.czrys.length == 0) {
                                    layer.alert('未找到对应操作人员信息,请与系统管理员联系。');
                                } else if(json.czrys.length > 1) {
                                    var content = "<select id=\"czry\" style=\"padding:5px 5px;border:solid 1px #ddd;vertical-align:middle; width:100%;\">";
                                    for ( var i = 0; i < json.czrys.length; i++) {
                                        var czry = json.czrys[i];
                                        content += "<option value=\"" + i + "\">" + czry.czrysfmc + "</option>";
                                    }
                                    content += "</select>";
                                    //弹框 显示身份 
                                    layer.open({
                                        fix : false, //不固定
                                        maxmin : true,
                                        title : '请选择操作人员',
                                        content : content,
                                        btn : [ "确定" ],
                                        yes : function(index, layero) {
    
    
                                            var czryIndex = layero.find('#czry').val();
                                            layer.close(index);
                                            var loadLayer = layer.load(2, {
                                              shade: [0.1,'#000'], //0.1透明度的白色背景
                                              time: 30000
                                            });
                                            //选择具体身份.
                                            $.ajax({
                                                url : _appPath + 'index/setCzry',
                                                data : {
   
   "params" : JSON.stringify({index : czryIndex})},
                                                type : 'post',
                                                dataType : 'json',
                                                success : function(obj) {
    
    
                                                    layer.close(loadLayer);
                                                    if (obj) {
                                                        window.location.replace(_appPath + "index.jsp");
                                                    } else {
                                                        parent.layer.msg("选择操作人员失败。", {icon : 2});
                                                    }
                                                }
                                            });
                                        }
                                    });
                                } else {
                                    window.location.replace(_appPath+"index.jsp");
                                }

                                $('#password').focus();
                            }
                        } 

以下代码是给选择身份的后台代码,

// FzUtil 类 获取session中的user 
    public static User getSession(HttpSession session) {
        SecurityContext securityContext = (SecurityContext) session
                .getAttribute("SPRING_SECURITY_CONTEXT");
        AuthUser authUser = (AuthUser) securityContext.getAuthentication()
                .getPrincipal();
        User user = authUser.getUser();
        return user;
    }

    //业务类 根据选择的身份 设置user中的身份索引
     @RequestMapping("/setCzry")
    @ResponseBody
    public boolean setCzry(HttpServletRequest request, Model model) throws CacheFrameException {
        Map m = (Map) getParamsData(request).getTBizData(Map.class);
        try {

            User user =FzUtil.getSession(request.getSession());
            user.setCzryIndex(Integer.valueOf(m.get("index").toString()));

       //问题就出在这     
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

发现session 未存储回去 user, 只是取出来没有set回去,没有setAttribute(); 与自己编码习惯不太一样,怀疑此处有问题.但是从Java内存模型上来考虑,及时不set回去也是可以的. 考虑到是否是两次请求打的节点不一样? 查看日志果然,权限不足的请求和之前登录的请求打到了两台服务器上,并且服务器上存在 BEA-100094警告. weblogic的session黏连.
因此考虑到问题的可能性,setAttribute才触发session复制, 用户登录时,创建session并且复制到了两个服务器上, 用户在操作时,第一次请求打1号服务器,修改了身份索引,并没有触发session复制(因为没有调用setAttribute()),在下一次触发session复制之前,用户请求打到了2号服务器,此时2号服务器中session存在,但是session中user对象的身份索引还是默认值.默认是0, 这就是上面提到的坑,就默认另一个身份执行了操作,显示权限不足.

6为了进一步确定问题,查看weblogic手册,session复制的介绍 .

在weblogic的文档中又翻出了以下内容:

Use setAttribute to Change Session State:
In an HTTP servlet that implements javax.servlet.http.HttpSession, use HttpSession.setAttribute (which replaces the deprecated putValue) to change attributes in a session object. If you set attributes in a session object with setAttribute, the object and its attributes are replicated in a cluster using in-memory replication. If you use other set methods to change objects within a session, WebLogic Server does not replicate those changes. Every time a change is made to an object that is in the session, setAttribute() should be called to update that object across the cluster.
翻译:
如果是集群环境下,session复制基于内存的, 想要改变一个session中对象的属性,需要使用 setAttribute方法, 如果你使用其他的方法来改变在一个会话对象,WebLogic Server不会复制这些变化。每次更改一个对象,在会议上,setattribute()应该被更新,在集群中的对象。

User user = session.getAttribute(KEY);
user.setCzry(czry);

在单机环境下,session中的变量user中的czry 属性就被更改了。然而在集群环境下,仅仅这样做是不能触发session同步机制的。必须要把user变量在重新放入session中,即

session.setAttribute(KEY, user);  

查看我们应用中使用的session复制策略是

 <persistent-store-type>replicated</persistent-store-type>

查看weblogic关于session复制的策略介绍:

在Weblogic中,HttpSession Replication的方式是通过在weblogic.xml中的session- descriptor的定义persistent-store-type来实现的. persistent-store-type可选的属性包括memory, replicated, replicated_if_clustered, async-replicated, async-replicated-if-clustered, file, async-jdbc, jdbc, cookie, coherence-web.

memory—Disables persistent session storage.
replicated—Same as memory, but session data is replicated across the clustered servers.
replicated_if_clustered—If the Web application is deployed on a clustered server, the in-effect persistent-store-type will be replicated. Otherwise, memory is the default.
async-replicated—Enables asynchronous session replication in an application or Web application. See “Asynchronous HTTP Session Replication” in Performance and Tuning for Oracle WebLogic Server.
async-replicated-if-clustered—Enables asynchronous session replication in an application or Web application when deployed to a cluster environment. If deployed to a single server environment, then the session persistence/replication defaults to in-memory. This allows testing on a single server without deployment errors.
file—Uses file-based persistence (See also session-descriptor).
async-jdbc—Enables asynchronous JDBC persistence for HTTP sessions in an application or Web application. See Configuring Session Persistence.
jdbc—Uses a database to store persistent sessions. (see also session-descriptor).
cookie—All session data is stored in a cookie in the user’s browser.
Coherence*-web For more information, see User’s Guide for Oracle Coherence*Web.

解释: replicated 是基于内存的. 那么就需要把对象回写回去.
因此确定需要setAttribute 回去. 查看系统中代码有大量没有setAttribute回去的.特别是操作USER对象的地方. 因此进行如下修改

HttpSession session =  request.getSession();
            SecurityContext securityContext = (SecurityContext) session
                    .getAttribute("SPRING_SECURITY_CONTEXT");
            AuthUser authUser = (AuthUser) securityContext.getAuthentication()
                    .getPrincipal();
            User user = authUser.getUser();
            user.setCzryIndex(Integer.valueOf(m.get("index").toString()));

            //集群环境下一定要set回去.
            session.setAttribute("SPRING_SECURITY_CONTEXT",securityContext);

总结:
集群环境与单机的区别,集群配置对代码的影响.
另外获取session使用request.getSession();也是不太好的.最好是request.getSession(false);
同时查看了系统中session的使用情况,发现使用很混乱,缺少架构级别的session管理.

猜你喜欢

转载自blog.csdn.net/liudashuang2017/article/details/79974091