GeekFactory

int128.hatenablog.com

AppEngine + Spring SessionでCookieが出力されない問題

AppEngine/Java + Spring Sessionを使っていて気づいたのですが、セッションの使い方によってクッキーにセッションIDが出力されない場合があります。具体的には以下の場合があります。

  • Spring SecurityのCSRFフィルタ:OK
  • Spring Securityのログイン:OK
  • ControllerでHttpSessionを操作した場合:OK
  • Controllerから @SessionScope Beanにアクセスした場合:BAD
  • Controllerで @SessionAttributes を使用した場合:BAD

10/21 追記:さらに調べたところ以下の条件になりました。

  • @ResponseBody を指定したControllerがStringを返した場合:OK
  • @ResponseBody を指定したControllerがintを返した場合:BAD
  • @ResponseBody を指定したControllerがObjectを返した場合:BAD

intやObjectは、JacksonによってJSONシリアライズされてストリームになります。すなわち、Controllerがストリームを返した場合はSpring Sessionが正しく動作しないようです。

OKは Set-Cookie: SESSION ヘッダが出力される、BADは出力されない、です。JARを実行した場合やWARをJettyで実行した場合はすべてOKになりました。Dev Server固有の問題のようです。

スレッドダンプでSpring Sessionの動作を調べたところ、OKとBADでCookieが付加されている箇所が異なるようです。具体的には、SessionRepositoryFilter で後続のフィルタでCookieが付与された場合はOK、そうでない場合はBADになります。

public class SessionRepositoryFilter<S extends ExpiringSession>
        extends OncePerRequestFilter {
//(snip)
        try {
            filterChain.doFilter(strategyRequest, strategyResponse);  // 後続でCookieが付与された場合はOK
        }
        finally {
            wrappedRequest.commitSession();  // ここでCookieが付与された場合はBAD
        }

FilterでHttpServletResponseを書き換えても実際のレスポンスに反映されない問題

AppEngineでは、ServletFilterで doFilter の後にHttpServletResponseを書き換えると実際のレスポンスに反映されないようです。doFilter の前に書き換えた場合はちゃんと反映されます。

例えば、下記の実装ではレスポンスに x-foo ヘッダは現れません。

@Component
public class ExampleFilter implements Filter {
    @Override public void init(FilterConfig filterConfig) throws ServletException {}
    @Override public void destroy() {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
        // doFilterの後
        val res = (HttpServletResponse) response;
        res.setHeader("x-foo", "bar");
    }
}

下記のように修正するとレスポンスに x-foo ヘッダが出力されるようになります。

        // doFilterの前
        val res = (HttpServletResponse) response;
        res.setHeader("x-foo", "bar");
        chain.doFilter(request, response);

Workaround

今のところ、以下のような対処しかなさそうです。

  1. ログイン後にセッションにアクセスする。
  2. 全リクエストで強制的にセッションを有効にする。
  3. セッションを使わない。

10/21 追記:

  1. @ResponseBody を指定した場合はStringを返すようにする。Jacksonを経由しないようにする。
  2. REST APIをステートレスに設計する。