记录一次不好追查的session冲突问题

问题现象

先给一个结果

本次问题是由于在使用request.getSession().getAttribute()时,在没有get到session时会创建一个新的cookie,叫做JSESSIONID,与shiro框架生成的cookie为同名,导致在根据“JSESSIONID”为name获取cookie时获取错误。

项目框架

SpringBoot+Cloud微服务,登录框架是shiro,数据处理层是mybatis,数据库是postgre不过不重要。

场景描述

  1. 在近期的项目中(Web),我们需要对所有接口做一个切面处理(AOP),其中一个内容是要获取当前用户的信息来做一个自动装入(以上这点现在提出来是基于我们已经排查到问题点为这里,所以才事后诸葛亮重点标识)。
  2. 问题的主要内容为登录后在服务间跳转的时候产生了session获取失败的情况。如果有类似情况的同学可以继续阅读,如果情况不类似可以自行选择是否继续阅读。
  3. 具体的过程是这样的,我们的项目服务主要分为四块:前台(后续用Web表示),后台(backstage),common通用类和模块,auth登录模块(即shiro)。项目通常的操作使用流程是:直接进入前台页面(Web,此时可以无需登录,直接访问无需登录权限的内容)->点击跳转到后台管理(backstage,后台管理必须是在登录的情况下才能进入)->登录->自由浏览前后台页面(权限校验,菜单目录权限等一系列,这些不重要不细谈了)。在我建立了AOP类并试图获取当前用户信息进行自动转入时,发现,在前台登录后,一切正常访问,但是此时点击跳转到后台时,会产生如下现象:
    • 在首次启动项目和浏览器时:可以正常登入后台。但是此时经过我们多次调试发现,在页面的Cookie中出现了两条名为JSESSIONID的内容(即Name为JSESSIONID, Value不同,此发现也是在多次排查下才发现的内容)。
    • 在首次登陆后,如果登出系统。再次登入时,可以正常进入前台正常浏览。但是当我们点击进入后台(也就是产生了服务间跳转:即项目路径发生变化),系统直接跳转到登陆页面。
    • 此后无论是重启项目还是重启浏览器都无济于事,无法正常进入后台系统。
      .根据浏览器控制台可以看到,在点击跳转到后台系统时,所有请求都还没有发送成功。

一些项目细节背景

1.  项目接口调用时,有filter将httpServletRequest做一次封装,封装为innerHttpRequest,目的是为了使用redisSession将用户信息(Cookie)保存到redis中进行存储。
2. 前后台登录通用一个JSESSIONID。
3. 进入后台时,会经过拦截器LoginInterceptor,用于校验是否已经登录。校验的方式是:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	if (request.getSession().getAttribute("realName") == null){
		//... 设置跳转到登陆页面的路径
		return false;
	}
}
也就是根据session获取当前用户登录信息。
4. 经过innerHttpServlet的封装后,request.getSession().getAttribute()则由原来的从request中获取session变为了从request中获取cookie的JSESSIONID,使用该ID到redis中查询用户信息。
5.  shiro设置session中的用户信息部分代码如下:
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception{
	UserDto activeUser = JSONUtils.parseObject(subject.getPrincipal().toString(),UserDto.class);
	CacheUtil cacheUtil = SpringUtils.getBean(CacheUtil.class);
	cacheUtile.remove(PHONE_LOGIN_CODE + token.getPrincipal());
	HttpSession session = ((HttpServletRequest) request).getSession();
	RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
	String id = session.getId();
	redisUtil.setMap(getSessionKey(id),"realName",activeUser.getRealName());
	redisUtil.setMap(getSessionKey(id),"user",JSONUtils.toJsonString(activeUser));
	log.info("login success sessionid = {}",id);
	//更新最近登录时间
	...
	return false;
}

排查历程

初步排查

在遇到该问题时,我首先对问题进行一些思考:
1. 出现该问题时所作的开发内容有哪些
	答:做了一部分业务代码微调,做了AOP功能去实现一些参数的自动写入。
2. 有哪些可能影响到登录的代码修改
	答:初步是没有想到任何能影响到登录模块的内容修改
3. 可能导致该问题的一些直观原因有哪些
	答: 首先考虑权限问题,是否因为权限配置受到影响,导致在拦截器被拦截到登录界面了。再考虑session冲突问题。

溯源排查

由于出现问题时,并没有意识到经过第一次登陆就会产生一个多余的JSESSIONID,导致后面无论怎么做都无法获取到正确的JSESSIONID,所以给调试造成了很多误区。
1. 当怀疑是AOP的问题时,首先尝试将AOP注释掉,不进行切面处理。但是问题依然存在。
2. 回滚了其他代码修改,但是仍未有任何变化。
3. 此时认为我自己的环境已经出现问题,为了确认这一点,我让同项目组的同事不要拉我的最新代码,进行尝试。结果在没有最新代码的情况下确实没有出现问题。
4. 所以目光还是放在AOP部分代码中。

调试排查

1. 进行代码调试排查。第一次排查将断点打在进入后台时的拦截器中LoginInterceptor,查看request的getSession结果。
	结果: request.getSession()并未获取到当前的登录用户的信息。但是此时如果用当前登录用户的sessionId在redis中进行查找,是可以找到对应的用户信息的。
2. 此时即可考虑两种情况:要么是request的session没有正确封装为innerHttpSession,导致没有去redis查询用户信息。要么是根本上获取的session就是错误的。此时考虑先看下request进来之后,查到的cookie是什么样的。一路追寻,在cookieManager处打断点,如下:
public String getIdFromHttp(HttpServletRequest request, HttpServletResponse response) {
	if(request == null){
		throw new RuntimeException("no request found");
	}
	//读取Cookie中的SessionId
	CookieManager cookieManager = new CookieManager(request,response);
	String sessionId = cookieManager.getValue(DEFAULT_SESSION_ID_NAME,null);
	if(!StringUtils.isBlank(sessionId)){
		return sessionId;
	}
	//获取参数中的SessionId
	sessionId = request.getParameter(DEFAULT_SESSION_ID_NAME);
	if(!StringUtils.isBlank(sessionId)){
		return sessionId;
	}
	return null;
}
断点结果显示,竟然获取到了两个cookie,且名字同为JSESSIONID。
3. 这样一下子就明白了,因为有一个多余的同名cookie(称之为脏cookie),所以获取到的sessionId是错误的,自然也没办法从redis中获取到相关用户信息。
4. 那么这个脏cookie是从哪里来的呢。经过多次调试,我发现了复现规律。重启电脑后的第一次登陆系统,是没问题的,但是再进行任何操作都会导致session变脏,登出系统。所以定位在了第一次登陆系统。
5. 除了第一次登陆系统,我尝试了去除可能产生影响AOP模块,再进行系统首登(这里首登是指,将脏cookie去除掉之后登陆。刚开始在请求中没有找到脏cookie,甚至ctrl+F5都不起作用。后来在浏览器的cookie列表中,也就是在地址栏的最左侧的小按钮中,才找到了脏cookie怎么去除),就不会出现该问题。所以问题锁定在了AOP内容中。
6. AOP中有哪些session相关的内容呢。首先可以确定,直接代码中是绝对没有操作cookie的。那么是不是有什么没有注意到的地方呢。这个时候好在有同事大神帮忙一起看问题,不然可能很难察觉到。AOP代码中有一段获取用户信息的代码,如下:
public UserDto getUserInfo(HttpServletRequest request){
	Object userJson = request.getSession().getAttribute("user);
	if(userJson != null){
		return JSONUtils.parseObject(userJson.toString(),userDto.class)}
	return null;
}
这里的getSession().getAttribute("user"); 本意是通过系统封装的innerHttpSession从redis里获取用户信息。但显然这里可能出了问题。
6.  于是尝试更改了新的获取用户方式,选择直接获取正确的Cookie,再直接从redis中拼接处mapKey获取用户信息。代码如下:
public UserDto getUserInfo(HttpServletRequest request){
	Cookie[] cookies = request.getCookies();
	if(cookies != null&&cookies.length>0){
		for(Cookie cookie : cookies){
			if("JSESSIONID".equals(cookies.getName())){
				String userJson = redisUtil.getMap("session_" + cookie.getValue(),"user");
				if(userJson != null){
					return JSONUtils.parseObject(userJson.toString(),userDto.class);
				}
			}
		}
	}
	return null;
}
如此尝试,确实解决了问题,没有再出现脏session。看来之前的getSession()一定有猫腻。

问题结果

结果分析

1. request.getSession()这个方法应该做一下追寻。在底层,如果当前没有获取到任何session的时候,这个方法会做一次创建session的操作,并且命名为JSESSIONID。具体的代码内容我仍在学习追查,后续会补上来。但是这个问题是值得大家注意的。
2. 至于为什么该request没有被封装为innerHttpSession,这个还是很难讲。因为我们尝试过,无论什么时候进入这个AOP获取到的session都是innerHttpRequest的子类,理论上应该成功的去redis获取信息才对。但是显然还有疑点在其中。
3. 所以一方面我们要看下getSession到底为什么会创建一个脏cookie,一方面要看下是不是innerHttpSession也有其中我们没有留意到的地方,创建了脏Cookie。
4. 最重要的,在使用shiro框架时,我们其实应该思考一下,创建Cookie时是不是应该用一个独有的name,JSESSIONID似乎是一个默认cookieName,很容易产生session冲突。这一点也会在后续的开发过程中进行尝试。

主要用于问题记录,如果对你有帮助,谢谢观看。
仍在更新。有相关资料文档会后续补上来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值