回想一下,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,默认的两个实现类为:

 

  1.     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传递问题。