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的一個缺陷吧?

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章