本文适用于对session、cookie有一定了解的同学,主要以问题定位过程为线索,简单讲述tomcat session生成机制、oauth2认证过程以及spring方法参数映射处理等内容
背景知识:
- session:由于http协议无状态,为了保存用户状态信息,web容器支持session管理机制,当客户端请求web应用时,如果在处理过程中调用了request.getSession()方法,则web容器会先根据url或者cookie中上传的JSESSIONID(默认,可以另行设置,该值会被设置到request对象的requestedSessionId字段)查找对应session,如果获取不到将自动创建一个session对象;当session过期或被放弃后,服务器将终止该session
protected Session doGetSession(boolean create) {
// 略去部分代码
(requestedSessionId != null) {
session = manager.findSession(requestedSessionId);
// 略去部分代码
String sessionId = getRequestedSessionId();
// 略去部分代码
session = manager.createSession(sessionId);
// 创建cookie并写入response
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
// 略去部分代码
}
- cookie:为了服务器能够识别不同的客户端,客户端通过cookie保存服务端返回的数据(如JSESSIONID,tomcat服务器默认的session对应的cookie key为JSESSIONID),然后服务端通过客户端请求时放在http header中的cookie数据找到之前为客户端创建的session;cookie分为会话cookie和持久cookie,会话Cookie浏览器会话有效期间存在,持久cookie服务端会设置http header 缓存相关字段指示客户端缓存策略,cookie传到客户端后,保存在某个目录下
- oauth2:oauth是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用,可以用于微服务环境下的公共鉴权,而本文服务鉴权走的就是oauth2
前人挖坑后人落,都是框架惹的祸
问题特征:
- 需求上线在即,发现某个接口请求后会新生成JSESSIONID并以Cookie形式回写浏览器,导致后续请求鉴权失败,必现
- 服务相互调用关系比较复杂,需要在开发环境测试该功能,本地没有测试
问题定位过程:
- 交流得知,此前定位过程中,将被请求方法体代码全部注销后,问题仍然存在,于是错过了查看问题代码(其实是方法参数出了问题),尽早定位出问题的好机会
- 怀疑是否nginx会话保持策略导致的问题(但是该策略应该会对所有请求均产生影响,而不只是单个请求),查看nginx.conf配置,该服务没有走负载均衡
- 怀疑是否nginx对该请求路径(路径中包含auth敏感字段)做了特殊配置,但查看配置,nginx对于该服务请求路径配置规则很简单,且将请求路径中auth字段删掉后测试,问题仍然存在
- 怀疑是否有外围代码重写了cookie,搜索没找到相关代码
- 梳理服务鉴权过程(见top图,图中做了缩略,将公共认证服务删除,直接对接oauth2),描述如下:
1)认证鉴权逻辑被封装在了公共Filter中,对所有请求进行拦截,判断是否已登录
2)如果没有上传JSESSIONID或者无法据其在redis找到session及token信息,返回客户端重定向到login接口
3)客户端调用login接口:服务端调用oauth2(其实有个公共的鉴权服务)认证合法性,将认证返回的token信息融合自身的JSESSIONID写入redis,随后将服务端JSESSIONID写回客户端cookie,坑:此处复用了tomcat默认的JSESSIONID,推测是为了复用request的getRequestedSessionId方法,该方法会直接取客户端cookie提交的JSESSIONID,业务中多次用到了getRequestedSessionId方法,tomcat session和服务认证返回session除了都叫JSESSIONID外,没有半毛钱关系,但是因为重名,相互之间会覆写
4)客户端带着认证返回的JSESSIONID继续调用之前的业务接口,通过认证,走业务逻辑,其实此时tomcat中的JSESSIONID和上传的JSESSIONID不一致,根据上传的JSESSIONID在tomcat是找不到对应session的
5)认证过程还有其他逻辑,此处做了删减,不影响主体流程 - 梳理之后,发现二者之间虽然key一致但是值不同,而且彼此会干扰,于是怀疑外围代码调用了getSession方法,搜索代码未找到
- 查看请求路径对应方法,方法中使用了session参数,但是方法体中未使用该参数,联想到spring的参数值自动映射机制(见ServletRequestMethodArgumentResolver),在为session赋值的过程中调用了getSession方法,进而由于在tomcat找不到对应session而新建、回写相关cookie到客户端
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
// 略去部分代码
if (HttpSession.class.isAssignableFrom(paramType)) {
HttpSession session = request.getSession();
// 略去部分代码
}
}
- 删除参数,测试,问题解决
坑:
- 使用该认证机制的服务的业务方法中不要使用tomcat的session机制(如getSession或session参数),否则就会出现这个问题
解决方案:
- 针对此问题,将session参数去掉
- 将认证机制的JSESSIONID换成另外一个名字,但是改动量比较大,可能会有其他坑
- 走分布式session解决方案,将tomcat的session管理机制和分布式session结合起来
- 走jwt token处理机制,服务端不再存储token,仅做鉴权和刷新