背景描述:
用户具有多个身份,比如一个人同时兼任技术经理又兼任项目经理,登录时会有多个身份.具体操作就是用户点击登录后 ,弹框选择身份, 不同身份具有不同的权限.
系统是采用两台服务做得集群,服务器是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管理.