Spring-Session + Struts2 无法写出Cookie: SESSION的一个巨坑

公司现有系统需要整合Spring Session + Spring data redis,整合后发现了一个问题,就是Spring session的 Cookie (名称为:SESSION无法写出到客户端浏览器,导致登录验证成功后又被LoginFilter验证再次强制重定向到登录页)。

查看Spring Session源码:发现写入Cookie都是通过以下方法实现的:

org.springframework.session.web.http.DefaultCookieSerializer.writeCookieValue

而这个方法什么时候会被调用呢?

调用链如下:

org.springframework.session.web.http.SessionRepositoryFilter#SessionRepositoryRequestWrapper的commitSession方法

调用:

org.springframework.session.web.http.CookieHttpSessionStrategy的OnNewSession方法

调用:

org.springframework.session.web.http.DefaultCookieSerializer的writeCookieValue方法

 

那么commitSession方法会在什么时候被调用呢?这个方法主要在2个地方调用:

第一个调用地方如下: 

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
				request, response, this.servletContext);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
				wrappedRequest, response);

		HttpServletRequest strategyRequest = this.httpSessionStrategy
				.wrapRequest(wrappedRequest, wrappedResponse);
		HttpServletResponse strategyResponse = this.httpSessionStrategy
				.wrapResponse(wrappedRequest, wrappedResponse);

		try {
			filterChain.doFilter(strategyRequest, strategyResponse);
		}
		finally {
			wrappedRequest.commitSession();
		}
	}

它在filter过滤链完成后被调用,而一般Spring Session的SessionRepositoryFilter建议是放在filter过滤链的第一个位置,那么上面方法在finlly中调用commitSession,将保证在最后执行过滤器SessionRepositoryFilter,commitSession一定会调用到。

虽然是放在过滤器的最后,一定会被调用,但是调用是否有效还要看情况,,调用是否有效取决于 response.isCommited()方法返回值的。

 

第二个调用地方:

private final class SessionRepositoryResponseWrapper
			extends OnCommittedResponseWrapper {

		private final SessionRepositoryRequestWrapper request;

		/**
		 * Create a new {@link SessionRepositoryResponseWrapper}.
		 * @param request the request to be wrapped
		 * @param response the response to be wrapped
		 */
		SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
				HttpServletResponse response) {
			super(response);
			if (request == null) {
				throw new IllegalArgumentException("request cannot be null");
			}
			this.request = request;
		}

		@Override
		protected void onResponseCommitted() {
			this.request.commitSession();
		}
	}

 

commitSession方法会被 OnResponseCommited方法调用,OnResponseCommited()是抽象模板方法,具体看SessionRepositoryResponseWrapper的父类:OnCommittedResponseRrapper类中的调用:

private void doOnResponseCommitted() {
		if (!this.disableOnCommitted) {
			onResponseCommitted();
			disableOnResponseCommitted();
		}
	}

 

doOnResponseCommited在什么时候被调用呢?这个被调用地方比较多了:

//在OnCommittedResponseWrapper的以下这些方法中会执行doOnResponseCommitted()

@Override
public final void sendError(int sc, String msg) throws IOException {
	doOnResponseCommitted();
	super.sendError(sc, msg);
}

@Override
public final void sendRedirect(String location) throws IOException {
	doOnResponseCommitted();
	super.sendRedirect(location);
}

@Override
public void flushBuffer() throws IOException {
	doOnResponseCommitted();
	super.flushBuffer();
}

 

而OnCommittedResponseRrapper类的内部类SaveContextPrintWriter和SaveContextServletOutputStream相关方法也会调用:

//OnCommittedResponseRrapper的内部类SaveContextPrintWriter的以下方法会调用doOnResponseCommitted
//OnCommittedResponseRrapper的内部类SaveContextServletOutputStream的以下方法会调用doOnResponseCommitted
public void flush() throws IOException {
	doOnResponseCommitted();
	this.delegate.flush();
}

public void close() throws IOException {
	doOnResponseCommitted();
	this.delegate.close();
}


//另外这2个内部类的相关print/write方法也都会调用doOnResponseCommitted()
public void print(String s) throws IOException {
	trackContentLength(s);
	this.delegate.print(s);
}
private void trackContentLength(String content) {
	checkContentLength(content.length());
}

//这个方法非常重要!!!
private void checkContentLength(long contentLengthToWrite) {
	this.contentWritten += contentLengthToWrite;
	boolean isBodyFullyWritten = this.contentLength > 0
		&& this.contentWritten >= this.contentLength;
	int bufferSize = getBufferSize();
	boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize;
	if (isBodyFullyWritten || requiresFlush) {
		doOnResponseCommitted();
	}
}

注意了: 以上这些方法在真正执行flush, close, 或者 print, write等方法前,会先执行doOnResponseCommitted(); 保证Cookie写出操作在 内容输出前 被执行。

因为response.isCommited()方法如果返回为true,那么往response中写入cookie是无效的,无法写入到客户端浏览器。API说明如下:

 

那么可以得出以下结论:

writeCookieValue()方法最终是被 commitSession()方法给调用的,

而commitSession最终会被OnResponseCommited方法调用,

而OnResponseCommited方法会被 以下场景调用:

1. response.flushBuffer()

2. response.sendRedirect()

3. response.sendError()

4. response.getWriter()或者response.getOutputStream() 的 close()方法

5. response.getWriter()或者response.getOutputStream() 的 flush()方法

6. response.getWriter()或者response.getOutputStream() 的 print()或者write()等相关内容输出方法调用

 

好了,回到坑的问题,为什么cookie无法被写出。

系统使用了: struts-json-plugin.jar 做为struts2的json实现

而公司系统把登录请求被做成了一个ajax请求。

所有的正常txt/html等正常跳转(非ajax请求)都能正常的写出 SESSION Cookie.

 

先看下登录Action 返回的JSON内容:

 

再看下Struts-json-plugin是如何输出json的,具体参考:

org.apache.struts2.json.JSONUtil的writeJSONToResponse方法:

public static void writeJSONToResponse(SerializationParams serializationParams) throws IOException {
    ....
    } else {
        response.setContentLength(json.getBytes(serializationParams.getEncoding()).length);
        PrintWriter out = response.getWriter();
        out.print(json);
    }
}

可以看出,writeJSONToResponse并没有主动调用close,或者 flush等方法。所以不会主动触发OnResponseCommited,

但是print和write方法其实也可以有条件性质的触发OnResponseCommited,如下面代码所示:

response.setContentLength 这个设置的值是 77,因为编码是UTF-8,返回结果中有4个汉字,1个汉字3个字节。

 json.getBytes(serializationParams.getEncoding()).length  返回值是77.

而out.print(json)方法中会进行判断,代码如下:

public void print(String s) throws IOException {
	trackContentLength(s);
	this.delegate.print(s);
}
private void trackContentLength(String content) {
	checkContentLength(content.length());
}

private void checkContentLength(long contentLengthToWrite) {
	this.contentWritten += contentLengthToWrite;
	boolean isBodyFullyWritten = this.contentLength > 0
		&& this.contentWritten >= this.contentLength;
	int bufferSize = getBufferSize();
	boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize;
	if (isBodyFullyWritten || requiresFlush) {
		doOnResponseCommitted();
	}
}

可以看到这里是按 json的length()去计算,计算出来contentLengthToWrite是69。

而bufferSize默认是取Response的缓冲区,大小大概是8000多。

那么checkContentLength永远都不会触发 doOnResponseCommitted(), 那么就无法正常写出cookie。

 

而如果把JSON串中的中文去掉或者改为英文,发现一切都正常了!!!

 

这应该算是spring-session的一个缺陷吧?

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值