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をステートレスに設計する。

JavaScriptにおけるURLエンコードされたCookieの取得

JavaScriptでURLエンコードされた文字列を変換するには decodeURIComponent を使用しますが、UTF-8以外の場合(Shift-JIS等)は文字コード変換が必要です。encoding.jsを使うと下記のように書けます。

github.com

Encoding.codeToString(Encoding.convert(Encoding.urlDecode(value), 'UNICODE', 'AUTO'));

Cookieを取得するにはjs-cookieが便利です。

github.com

Converterを使うと前処理で文字コードを変換しておくことが可能です。

Cookies.withConverter({
  read: value => Encoding.codeToString(Encoding.convert(Encoding.urlDecode(value), 'UNICODE', 'AUTO'));
}).get();

ご参考まで。

ZipkinのトレースIDを生成して引き継ぐ

ZipkinのトレースIDを引き継ぐ方法のメモです。

ブラウザ or 外部サービス
↓
フロントエンド(トレースIDを生成)
↓
↓ X-B3-TraceId: xxxx
↓
バックエンドAPI(トレースIDを中継)
↓
↓ X-B3-TraceId: xxxx
↓
バックエンドAPI

フロントエンドがPHPの場合は以下のようにトレースIDを生成すると簡単です。

// Generate 128bit random value
$traceId = bin2hex(openssl_random_pseudo_bytes(16));
define('ZIPKIN_TRACE_ID', $traceId);

// Add header (depends on API client)
$apiClient->addHeader('X-B3-TRACEID', ZIPKIN_TRACE_ID);

ブラウザにもトレースIDをヘッダで返しておくと、問題発生時の調査で役に立つでしょう。

Springの場合は依存関係に spring-cloud-starter-sleuth を追加すると以下を行ってくれます。

  • リクエストヘッダに X-B3-TRACEID がある場合はAPIクライアントに引き継ぐ。そうでない場合はトレースIDを生成する。
  • APIクライアントのリクエストヘッダに X-B3-TRACEID を付与する。
  • ログにトレースIDやスパンIDを出力する。