回想一下,Spring cloud微服务框架曾使用两年之久,为什么以前没有这种情况发生呢?
仔细梳理了以前使用的场景,用户在请求业务服务之前,必须先进行系统登录,在用户登录校验请求的时候,创建系统Session而且这种登录校验过程中不涉及跨服务使用Session的情况,在用户登录校验通过以后,用户再请求业务时其实Session已经创建好了,不涉及Session创建,故没有触发上面的情况,的确实际业务使用中很少涉及这种情况,也就让人觉得使用很顺利,一切OK的错觉。
填补这个坑,想到了如下两种方式:
- 创建Session与使用Session不放到一次请求中
也就是业务上规避一下,如果请求发现Session没有创建,说明用户可能没有登录过,可以创建Session后,将其请求重定向到Home页面或登录页面,这样下次业务数据请求时就可以直接使用Session了。这也是以前使用中没有注意到这个问题原因。
- ZUUL重新设置Cookie中SESSION ID
在此感谢HUIQQ0927提供的另外一种更便捷的方式,直接将默认的Cookie中的SESSIONID覆盖掉,重新设置新生成的SESSION ID。如下所示:
public Object run() throws ZuulException {
Double rand = Math.random() * 100;
int randInt = rand.intValue();
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpSession session = request.getSession();
if(!request.isRequestedSessionIdValid()) {
String sessionBase64 = Base64.getEncoder().encodeToString(session.getId().getBytes());
ctx.addZuulRequestHeader("Cookie", "SESSION=" + sessionBase64);
LOG.info("Session Base64:{}",sessionBase64);
// ctx.addZuulRequestHeader(SESSION_ID, session.getId());
}
session.setAttribute("test", randInt);
LOG.info("Session id:{} test:{}",session.getId(),randInt);
return null;
}
不过需要注意的是这个处理将整个Request请求中的Cookie都覆盖了,通常我们请求中Cookie中不仅仅存放一个SESSION ID,还会有其他业务或如SSO其他系统共享的Cookie,这样处理起来就会比较麻烦,需要先获取原有Request的Cookie获取到,然后修改或添加SESSION ID到原有的Cookie,最后再设置到请求的Header中。
由于是演示,故上面这段代码没有处理原有Cookie信息,实际应用中需要特别慎重处理,否则将引起不必要的麻烦。
- 重新实现CookieId获取策略
Spring boot在请求进来时对Request进行了包装,而在获取Session id时目前Spring boot支持两种,一种Cookie方式,也就是通常默认使用的方式;还有一种是header模式,通过request.getHeader()获取Session ID。
Spring boot定义了Session id获取的接口org.springframework.session.web.http.HttpSessionIdResolver,默认的两个实现类为:
- org.springframework.session.web.http.CookieHttpSessionIdResolver
该接口为默认的Spring boot获取Session方法,从Cookie中获取相应的Session id。
2. org.springframework.session.web.http.HeaderHttpSessionIdResolver
该接口通过从Request Header中获取session id,对其感兴趣的话可以查询Spring boot源码进行查看详细内容。
既然Spring boot提供了这个接口,那我们就可以自定义实现一个HttpSessionIdResolver接口,来获取自定义的Session id,下面我们应该怎么实现这个接口呢?
Spring boot默认使用CookieHttpSessionIdResolver进行操作,这样我们就有了参考,因为该类为Final类,不能够继承,故需要将其源码Copy到自定义类中,然后修改其id获取方式,那我们应该以什么方法获取到Session id呢?
我们可以将Header与Cookie两种结合方式来获取,即在Session创建的服务类中,将Session id设置到request header中,而在使用Session的服务类中,定义CustomHttpSessionIdResolver类,先从request header中获取session id,如果获取不到,再从cookie中获取,这样保证了Session id任何时候都能获取到,然后通过Session id也能获取到正确的Session对象了。
具体实现如下:
ZUUL服务中Filter中如果发现创建了Session则将Session id设置到request header中.
@Component
public class LoginFilter extends ZuulFilter {
private final static Logger LOG = LoggerFactory.getLogger(LoginFilter.class);
private final static String SESSION_ID = "SESSIONID";
@Override
public Object run() throws ZuulException {
Double rand = Math.random() * 100;
int randInt = rand.intValue();
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpSession session = request.getSession();
if(!request.isRequestedSessionIdValid()) {
ctx.addZuulRequestHeader(SESSION_ID, session.getId());
}
session.setAttribute("test", randInt);
LOG.info("Session id:{} test:{}",session.getId(),randInt);
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public String filterType() {
return "pre";
}
}
业务服务中,定制获取Cookie的类,如下:
package com.king.business;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.HttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class CustomHttpSessionIdResolver implements HttpSessionIdResolver {
private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class
.getName().concat(".WRITTEN_SESSION_ID_ATTR");
private final static String SESSION_ID = "SESSIONID";
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
String sessionId = request.getHeader(SESSION_ID);
if(!StringUtils.isEmpty(sessionId)) {
return Collections.singletonList(sessionId);
}
return this.cookieSerializer.readCookieValues(request);
}
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));
}
@Override
public void expireSession(HttpServletRequest request, HttpServletResponse response) {
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, ""));
}
/**
* Sets the {@link CookieSerializer} to be used.
*
* @param cookieSerializer the cookieSerializer to set. Cannot be null.
*/
public void setCookieSerializer(CookieSerializer cookieSerializer) {
if (cookieSerializer == null) {
throw new IllegalArgumentException("cookieSerializer cannot be null");
}
this.cookieSerializer = cookieSerializer;
}
}
这样通过配合完美的解决了Spring cloud单次服务Session传递问题。