HttpSession with Spring Sessionの挙動検証

Updated: / Reading time: 5 minutes

先日はServlet APIにおけるセッション管理の挙動を検証しました。今回は、Servlet APIにSpring Sessionを組み込んだ時の挙動を検証します。

検証するWebアプリケーション

今回のWebアプリケーションも先日と同様、アクセスするとcount=1count=2count=3とカウントアップするだけのWebアプリケーションです。

PlantUML SVG diagram

検証のために作成したWebアプリケーションは、次のリポジトリにあります。検証では詳細に説明はしないので、READMEやソースコードをご覧ください。

検証

Spring Sessionを組み込んだだけで、何も設定しない場合

Spring Session - HttpSession (Quick Start)を参考に実装を進めます。この検証では説明をいろいろ省略するので、詳細はSpring Session - HttpSession (Quick Start)をご覧ください。

依存関係を追加

pom.xmlの依存関係に、次の設定を追加します。

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>1.3.1.RELEASE</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>biz.paluch.redis</groupId>
    <artifactId>lettuce</artifactId>
    <version>3.5.0.Final</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.4.RELEASE</version>
</dependency>

Spring Java Configurationを作成

Spring設定を行うConfigクラスを作成します。src/main/java/me/u6k/sample/sample_spring_session_with_servlet/Config.javaファイルを次のように作成します。

package me.u6k.sample.sample_spring_session_with_servlet;

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession
public class Config {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}

@EnableRedisHttpSessionによって、HttpSessionをSpring Sessionで置き換えます。

LettuceConnectionFactoryによって、localhost:6379で待ち受けるRedisに接続するように設定されます。

Servletコンテナを初期化

先ほどのSpring設定を適用するため、Servletコンテナの初期化を行うInitializerクラスを作成します。src/main/java/me/u6k/sample/sample_spring_session_with_servlet/Initializer.javaファイルを次のように作成します。

package me.u6k.sample.sample_spring_session_with_servlet;

import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;

public class Initializer extends AbstractHttpSessionApplicationInitializer {
    public Initializer() {
        super(Config.class);
    }
}

コンストラクタのsuper()に先ほどのConfigクラスを渡すことで、Spring設定を適用します。

セッションを使用する

HttpSessionが内部的に置き換えられているため、使い方は普段と同じです。

package me.u6k.sample.sample_spring_session_with_servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@SuppressWarnings("serial")
@WebServlet("/")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        if (session.getAttribute("count") == null) {
            session.setAttribute("count", 0);
        }

        int count = (Integer) session.getAttribute("count") + 1;
        session.setAttribute("count", count);

        resp.setContentType("text/plain");
        resp.getWriter().write("count=" + count);
    }
}

req.getSession()HttpSessionを取得して、getAttribute()setAttribute()でセッションに値を入出力します。内部的にはSpring Sessionが使用され、Redisに値が保存されます。

実行する

まず、Redisを起動します。

$ docker run --name redis -d -p 6379:6379 redis

この時点のRedisは空ですが、空であることを確認します。Redisの内容を確認するには、redis-cliを使用します。

$ docker run -it --link redis:redis --rm redis redis-cli -h redis -p 6379

全てのキーを確認してみます。

redis:6379> keys *
(empty list or set)

空であることが確認できます。

次に、Webアプリケーションを起動します。

$ ./mvnw jetty:run

http://localhost:8080/ にアクセスすると、count=1が表示されます。再びアクセスするとcount=2が表示されるはずです。つまり、セッションに値が保存されます。

redis session 01

redis session 02

この時のRedisの内容を確認します。再び、全てのキーを確認してみます。

redis:6379> keys *
1) "spring:session:sessions:expires:b3d335ac-e57e-4a18-a87f-92282aded73e"
2) "spring:session:sessions:b3d335ac-e57e-4a18-a87f-92282aded73e"
3) "spring:session:expirations:1512043860000"

セッションに対応するキーが作成されていることが分かります。

Set-CookieExpiresが設定されていないため、このセッションはWebブラウザを閉じるとWebブラウザから削除されます。Webブラウザを開いて再び http://localhost:8080/ にアクセスすると、またcount=1が表示されます。つまり、新しいセッションが作成されます。この時のRedisの内容を確認すると、次のようにキーが増えていることが分かります。

redis session 03

redis:6379> keys *
1) "spring:session:sessions:expires:5e50fc3c-7f05-431c-840f-93f1a0d0b8e0"
2) "spring:session:sessions:expires:b3d335ac-e57e-4a18-a87f-92282aded73e"
3) "spring:session:sessions:b3d335ac-e57e-4a18-a87f-92282aded73e"
4) "spring:session:sessions:5e50fc3c-7f05-431c-840f-93f1a0d0b8e0"
5) "spring:session:expirations:1512043980000"
6) "spring:session:expirations:1512044040000"

セッション・タイムアウトを設定した場合

HttpSessionをそのまま使う場合のセッション・タイムアウトは、web.xml<session-timeout>に設定しますが、Spring Session with Redisを使用する場合は、@EnableRedisHttpSessionmaxInactiveIntervalInSecondsに設定します。パラメータ名の通り、秒単位です。例えば、セッション・タイムアウトを3分に設定する場合は、以下のように設定します。

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 150)
public class Config {
(中略)
}

修正したら、先程と同様にRedisを起動して、Webアプリケーションを起動します。起動して http://localhost:8080/ にアクセスすると、count=1が表示されます。Set-CookieにはExpiresは設定されません。

session timeout 01

この時のRedisの内容を確認します。

redis:6379> keys *
1) "spring:session:sessions:expires:b7f0fc5f-ccc3-4959-9698-803760bc48a8"
2) "spring:session:expirations:1512043320000"
3) "spring:session:sessions:b7f0fc5f-ccc3-4959-9698-803760bc48a8"

分かりづらいですが、spring:session:expirations:xxxの値は、最終アクセス時刻から3分後を示しています。

試しにこの状態で3分以上放置してから、再びRedisの状態を確認してみます。

redis:6379> keys *
1) "spring:session:sessions:b7f0fc5f-ccc3-4959-9698-803760bc48a8"

キーが減りました。つまり、セッションがサーバー側で破棄されました。Webブラウザで http://localhost:8080/ にアクセスすると、再びcount=1が表示されます。

session timeout 02

Redisの状態を確認すると、新しいセッションのキーが増えていることが分かります。

redis:6379> keys *
1) "spring:session:sessions:expires:e82d2bde-75f7-491f-b1a6-87c2c6eacf85"
2) "spring:session:sessions:e82d2bde-75f7-491f-b1a6-87c2c6eacf85"
3) "spring:session:expirations:1512043560000"
4) "spring:session:sessions:b7f0fc5f-ccc3-4959-9698-803760bc48a8"

Cookie寿命を設定した場合

セッション・タイムアウトを1分、Cookie寿命を2分に設定した時の挙動を確認します。

HttpSessionをそのまま使う場合のCookie寿命は、web.xml<cookie-config>/<max-age>に設定しますが、Spring Session with Redisの場合は、CookieSerializerインスタンスを構築することで設定します。

具体的には、以下のように設定します。

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60)
public class Config {
(中略)

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieMaxAge(120);
        return serializer;
    }
}

詳細は Spring Session - Custom Cookie をご覧ください。

修正したら、先程と同様にRedisを起動して、Webアプリケーションを起動します。起動して http://localhost:8080/ にアクセスすると、count=1が表示されます。この時のSet-CookieではExpiresMax-Ageが設定されます。

cookie maxage 01

1分以内に同様にアクセスすると、カウントアップされ続けます。

cookie maxage 02

Webブラウザを更新し続けていると、最初のアクセスから2分経過後にcount=1に戻ります。

cookie maxage 03

この時のRedisを確認すると、最初のセッション・キーと2回目のセッション・キーの両方が存在します。最初のセッション・キーはこの時点で消滅していないためです。1分ほど放置すると、最初のセッション・キーは消滅します。

redis:6379> keys *
1) "spring:session:sessions:expires:e9e0bb2a-c003-468a-b99d-ab5fcfae97d7"
2) "spring:session:sessions:expires:cd116f8e-ed1f-478a-8774-42c5cb4fbbe5"
3) "spring:session:expirations:1512044820000"
4) "spring:session:sessions:cd116f8e-ed1f-478a-8774-42c5cb4fbbe5"
5) "spring:session:sessions:e9e0bb2a-c003-468a-b99d-ab5fcfae97d7"

おわりに

Redisと連携した時のセッション管理がどのように行われるのかが整理できました。次はSpring Securityを食い込んだ時のセッション管理か、Spring Securityによるサインアップかな。