Open13

Spring Session (2.3.1.RELEASE) について調べたあれこれ

daisuzzdaisuzz

Spring Sessionによって透過的に扱うことができる情報

  • HttpSession
    • アプリケーションサーバで保持していたセッション情報を共有のキャッシュストレージへ置き換えることが可能になる
  • WebSockets
    • WebSocketsメッセージを受け取ったときにHttpSessionを有効にし続けることが可能になる
  • WebSession
    • SpringWebFluxのWebSessionをアプリケーションコンテナに依存しない方法で置き換えることが可能になる

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#introduction

daisuzzdaisuzz

Http Sessionとの統合

  • Spring Sessionを使うと、既存のHttpSessionインターフェースの実装クラスをSpring Sessionによって提供されるHttpSessionインターフェースの実装クラスに置き換えることができる
  • Spring Sessionによって提供されるHttpSessionインターフェースの実装クラスに置き換えるメリット
    • clustered sessionを簡単にサポートできる
    • RESTful APIと連携するためにヘッダでSession IDを提供することができる
  • Spring Sessionが提供するHttpSession実装クラスを有効にする方法
    • @EnableRedisHttpSesionをJavaConfigに付与する
        @EnableRedisHttpSession 
        public class Config {
        
        }
        ```
    
    • @EnableRedisHttpSessionをJavaConfigに付与することでSessionRepositoryFilterのBeanが作られる
  • SessionRepositoryFilter
    • Servlet Filterインターフェースの実装クラス
    • HttpSessionをSpring Sessionが提供しているカスタム実装に置き換える
    • AbstractHttpSessionApplicationInitailizerを継承したクラスを実装することで、SessionRepositoryFilterがServlet Containerに登録される
    public class Initializer extends AbstractHttpSessionApplicationInitializer { 
    
      public Initializer() {
      	super(Config.class); 
      }
    
    }
    
    • AbstractHttpSessionApplicationInitalizer
      • SpringがJavaConfigを読み込むことを保証する仕組みを提供する

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession

daisuzzdaisuzz

HttpSession との統合の仕組み

  • Spring SessionはHttpSession, HttpServletRequest, HttpServletResponseを実装することでセッションの機能を提供している
  • SessionRepositoryRequestWrapper
    • HttpServletRequestWrapperを継承
    • HttpSessionを返すメソッドはoverride
    • それ以外のメソッドはHttpServletRequestWrapperによって実装されている
      • 元のHttpServletRequestの実装に処理を委譲している
    • SessionRepositoryを使ってHttpSessionに対して操作を行う機能をHttpServletRequestWrapperに追加したクラス
    • セッションの永続化や、セッションIDの変更、セッションの取得などを行うことができる
    • SessionRepositoryFilterの処理の中でHttpServletRequestの実装をカスタム実装されたHttpServletRequestに置き換えている
  • カスタム実装されたHttpServletRequestをFilterChainに渡すことで、SessionRepositoryFilter以降で呼び出される処理でカスタムHttpSessionが使われるようになる

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-how

daisuzzdaisuzz

HttpSessionListenerを利用する

  • HttpSessionListenerとは
    • HttpSessionのライフサイクルの変更に関するイベントを受け取るためのインターフェース
      • セッションが作成されたときと、セッションが破棄されたときのイベントを受け取るメソッドが定義されている
  • Spring Session はHttpSessionListenerをサポートしている
    • SessionEventHttpSessionListenerAdapterを宣言することによって、SessionDestroyedEventとSessionCreatedEventをHttpSessionEventに変換することができる
  • HttpSessionListenerをサポートするために必要なこと
    • SessionRepositoryの実装クラスが、SessionDestroyedEventとSessionCreatedEventを発火するように設定されていること
    • SessionEventHttpSessionListenerAdapterをBeanとして定義すること
    • 全てのHttpSessionListenerをSessionEventHttpSessionListenerAdapterに挿入すること
  • JavaConfigで設定を行う場合、全てのHttpSessionListenerをBeanとして定義すればよい
    • Spring Securityのconcurrency controlに対応し、HttpSessionEventPublisherを使う必要がある場合は、以下のような設定を行う
    @Configuration
    @EnableRedisHttpSession
    public class RedisHttpSessionConfig {
    
    	@Bean
    	public HttpSessionEventPublisher httpSessionEventPublisher() {
    		return new HttpSessionEventPublisher();
    	}
    
    	// ...
    
    }
    

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-httpsessionlistener

daisuzzdaisuzz

Spring Securityとの統合

  • Spring SessionはSpring Securityとの統合機能を提供している
  • Spring Security Remember-me認証のサポート
    • セッションの有効期限の変更
    • セッションCookieがInteger.MAX_VALUEで期限切れになることを保証する
    • セッションCookieの有効期限は可能な限り最大の値に設定される
      • セッションCookieはセッションの作成時のみ設定されるため
    • セッションの有効期限と同じ値に設定されている場合は、ユーザが利用したときにセッションが更新される
      • Cookieの有効期限は更新されない
    • Java ConfigをつかってSpring SecurityとSpring Sessionを統合させる方法の例
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http
    		// ... additional configuration ...
    		.rememberMe((rememberMe) -> rememberMe
    			.rememberMeServices(rememberMeServices())
    		);
    }
    
    @Bean
    public SpringSessionRememberMeServices rememberMeServices() {
    	SpringSessionRememberMeServices rememberMeServices =
    			new SpringSessionRememberMeServices();
    	// optionally customize
    	rememberMeServices.setAlwaysRemember(true);
    	return rememberMeServices;
    }
    
  • Spring Security Concurrent Session Controlのサポート
    • 一人のユーザが同時に持つことができるアクティブなセッションの数を制限することができる
    • Spring Securityのデフォルトのサポートと違い、クラスター環境でも機能する
    • Spring SecurityのSessionRegistryインターフェースのカスタム実装をSpring Sessionが提供することで実現する
    • Java Config をつかってカスタム実装されたSessionRegistryをSessionManagementConfigurerに設定する方法の例
    @Configuration
    public class SecurityConfiguration<S extends Session> extends WebSecurityConfigurerAdapter {
    
    	@Autowired
    	private FindByIndexNameSessionRepository<S> sessionRepository;
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		// @formatter:off
    		http
    			// other config goes here...
    			.sessionManagement((sessionManagement) -> sessionManagement
    				.maximumSessions(2)
    				.sessionRegistry(sessionRegistry())
    			);
    		// @formatter:on
    	}
    
    	@Bean
    	public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
    		return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
    	}
    
    }
    
    • Sessionインスタンスを返すFindByINdexNameSessionRepositoryを提供するためにSpringSessionを設定していることを前提としている
  • Spring Sessionが提供しているSessionRegistryインターフェースの実装クラスは、getAllPrincipalメソッドをサポートしていないので注意
    • Spring Sessionを使ってgetAllPrincipalメソッドに必要な情報が取得できないため
    • Spring Securityではこのメソッドを利用しないため、問題になるのはアプリケーション側で直接SessionRegistryにアクセスするケースのみ

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#spring-security

daisuzzdaisuzz

SessionとSessionRepositoryについて

Session

  • セッションID、セッションに紐づいた属性、セッションの有効期限などに対する操作をもつインターフェース
  • 使用例
    public class RepositoryDemo<S extends Session> {
    
    	private SessionRepository<S> repository; 
    
    	public void demo() {
    		S toSave = this.repository.createSession(); 
    
    		
    		User rwinch = new User("rwinch");
    		toSave.setAttribute(ATTR_USER, rwinch);
    
    		this.repository.save(toSave); 
    
    		S session = this.repository.findById(toSave.getId()); 
    
    		
    		User user = session.getAttribute(ATTR_USER);
    		assertThat(user).isEqualTo(rwinch);
    	}
    
    	// ... setter methods ...
    
    }
    
    public class ExpiringRepositoryDemo<S extends Session> {
    
    	private SessionRepository<S> repository; 
    
    	public void demo() {
    		S toSave = this.repository.createSession(); 
    		// ...
    		toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); 
    
    		this.repository.save(toSave); 
    
    		S session = this.repository.findById(toSave.getId()); 
    		// ...
    	}
    
    	// ... setter methods ...
    
    }
    

SessionRepositoryインターフェース

  • Sessionを利用するための最も基本のAPI
  • Sessionインスタンスの作成, 取得, 永続化をおこなう
  • 可能であれば、SessionRepositoryやSessionを直接操作するべきではなく、HttpSessionやWebSocketとの統合を通して間接的にSessionRepositoryやSessionを操作すべき
  • SessionRepositoryは機能がとてもシンプルなため、追加の機能が拡張された実装が提供されている

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#api-session

daisuzzdaisuzz

FindByIndexNameSessionRepositoryについて

  • SessionRepositoryを機能拡張したインターフェース
  • 引数で与えたインデックス名やインデックスの値に紐づく全てのセッションを取得する機能を提供
    • 一般的なユースケースとしては、特定のユーザの全てのセッションを検索するケースがある
  • FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAMEという名前のセッション属性にユーザ名が入力されていることを保証することで実現されている
  • Spring Sessionは使われている認証メカニズムを認識していないため、属性が設定されているかどうかを確認することは開発者の責務
    • セッションをusernameに設定する例
    String username = "username";
    this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
    
    • usernameに紐づくセッション情報を取得する例
    String username = "username";
    Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
    
  • FindByIndexNameSessionRepositoryの実装クラスの中には、他のセッション属性に自動的にインデックスをつけるためのフックを提供する
    • 多くの実装では、自動的に現在のSpring Securityユーザ名がFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAMEのインデックス名でインデックス付けされていることを自動的に確認する

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#api-findbyindexnamesessionrepository

daisuzzdaisuzz

@EnableSpringHttpSessionについて

  • Java Configに付与するアノテーション
  • 付与することで、SessionRepositoryFilterをspringSessionRepositoryFilterという名前でBean定義してくれる
  • このアノテーションを使うために、SessionRepositoryを一つBean定義する必要がある
  • Java Configの例
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {

	@Bean
	public MapSessionRepository sessionRepository() {
		return new MapSessionRepository(new ConcurrentHashMap<>());
	}

}
  • この設定では、セッションの有効期限のためのクラスは設定されていないことに注意
    • セッションの有効期限のようなものは実装に大きく依存するため
  • 有効期限が切れたセッションをきれいにするためには、開発者が明示的に設定する必要がある

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#api-enablespringhttpsession

daisuzzdaisuzz

RedisIndexedSessionRepositoryについて

  • Spring DataのRedisOperationsを使って実装されたFindByIndexNameSessionRepositoryの実装クラス
  • Web環境では、通常SessionRepositoryFilterと組み合わせて使われる
  • SessionMessageListenerを介してSessionDestroyedEventとSessionCreatedEventをサポートする
  • RedisIndexedSessionRepositoryの初期化処理の例
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

// ... configure redisTemplate ...

SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
  • @EnableRedisHttpSession
    • Web環境ではJavaConfigに@EnableRedisHttpSessionを付与すれば、RedisIndexedSessionRepositoryを作成してくれる
    • RedisIndexedSessionRepositoryを設定するための属性は以下
      • maxInactiveIntervalInSeconds
        • セッションが期限切れになるまでの時間(秒単位)
      • redisNamespace
        • セッションのアプリケーション固有の名前空間
        • RedisキーとチャネルIDはここで設定した値がプレフィックスとして付与される
      • flushMode
        • データがRedisに書き込まれるタイミング
        • デフォルトはSessionRepositoryのsaveメソッドが呼ばれたタイミング
        • FlushMode.IMMEDIATEを設定すると、なるべく早くRedisに書き込む
  • RedisSerializer<Object>を実装したspringSessionDefaultRedisSerializerという名前のBeanを定義することでRedisのserializationをカスタマイズできる
  • TaskExecutor
    • RedisIndexedSessionRepositoryは、RedisMessageListenerContainerを使って、Redisからイベントを受け取るようになっている
    • Redisからのイベントをdispatchする方法は、springSessionRedisTaskExecutorと springSessionRedisSubscriptionExecutorという名前のをBeanを定義することでカスタマイズできる
  • Redisがどのように更新されるか
    • セッションを作成するときには以下のようなコマンドが実行される
    HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
    	maxInactiveInterval 1800 \
    	lastAccessedTime 1404360000000 \
    	sessionAttr:attrName someAttrValue \
    	sessionAttr2:attrName someAttrValue2
    EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
    APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
    EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
    SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
    EXPIRE spring:session:expirations1439245080000 2100
    
    • セッションを格納する
      • 各セッションはRedisにHashとして格納される
      • HMSETコマンドを使って各セッションの格納したり更新する
      • セッションを格納する例
      HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
        	maxInactiveInterval 1800 \
        	lastAccessedTime 1404360000000 \
        	sessionAttr:attrName someAttrValue \
        	sessionAttr2:attrName someAttrValue2
      
      • セッションID
        • 33fdd1b6-b496-4b33-9f7d-df96679d32fe
      • セッションの作成時間
        • 1404360000000 ミリ秒
        • UNIX時間
          • 1/1/1970 00:00 GMTからの経過時間
      • セッションの有効期間
        • 1800 秒
      • セッションが最後にアクセスされた時間
        • 1404360000000 ミリ秒
        • UNIX時間
      • セッションの属性
        • attrName:someAttrValue
        • attrName2:someAttrValue2
    • セッションを更新する
      • RedisIndexedSessionRepositoryによって管理されるSessionインスタンスは変更されたプロパティのみを指定して更新する
      • attrName2属性が更新されたときはRedisへの保存時に以下のコマンドが実行される
      HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
      
    • セッションの有効期限を設定する
      • 有効期限はSession.getMaxInactiveInterval()に基づいた、EXPIREコマンドを実行することで各セッションに紐づけられる
      • EXPIREコマンドの例
      EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
      
      • セッションが期限切れになったあとに有効期限を5分(1800s→2100s)に設定している点に注意

      • これは、セッションの有効期限が切れた時にセッションの値にアクセスできるようにするために必要な設定

      • 必要な処理を実行したあとで、セッションがクリーンアップされることを確認するために、実際に有効期限が切れてから5分後にセッション自体に設定される

      • 以下がドキュメントに記載されている本文

        Note that the expiration that is set to five minutes after the session actually expires. This is necessary so that the value of the session can be accessed when the session expires. An expiration is set on the session itself five minutes after it actually expires to ensure that it is cleaned up, but only after we perform any necessary processing.

      • SessionRepository.findById(String)メソッドは期限が切れたセッションを返さないようになっている

        • これによってセッションを使う前に有効期限を確認する必要がなくなる
    • Spring SessionはRedisから通知される削除と期限切れのkeyspace notificationを受けて、SessionDeletedEventとSessionExpiredEventを発火させる
    • SessionDeletedEventとSessionExpiredEventはセッションに紐づいているリソースがクリーンアップされていることを確認する
      • Spring SessionのWebSocketサポートを使う場合、Redisの削除あるいは期限切れのイベント通知によって、セッションに紐づくWebSocketの接続が全てcloseされる
    • 有効期限を迎えると、セッションデータがもはや利用できなくなるので、有効期限はsession key自体で直接管理されない
    • 代わりにsession expires keyという特別なキーが使われる
    • session expires keyの例
    APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
    EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
    
    • session expires keyが削除されたり、有効期限を迎えると、keyspace notificationが実際のセッションのルックアップをトリガーし、SessionDestroyedEventが発火される
    • Redisの有効期限のみに依存する問題の1つは、キーにアクセスしていない場合、Redisが期限切れのイベントをいつ発火させるか保証しないこと
      • Redisが期限切れのキーをクリーンアップするために使うバックグラウンドタスクは優先度が低いタスクのため、有効期限を迎えた時点ではキーの期限切れイベントをトリガーしない場合がある
      • 詳細は https://redis.io/topics/notifications を参照
        • Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero.

        • Redis側ではあくまで、Redisサーバがキーを削除したタイミングで期限切れイベントが発火される仕組みになっている
    • 期限切れイベントの発火が保証されないという問題を解消するために、各キーが期限切れになると予想されるときに、各キーにアクセスするような仕組みを提供
    • キーの有効期限が切れた場合、Redisはキーを削除して、キーにアクセスしようとしたときに期限切れイベントを発生させる
    • 各セッションの有効期限も最も近い時間まで追跡される
    • バックグラウンドタスクが期限切れの可能性のあるセッションにアクセスして、Redisの期限切れイベントがより決定的な方法で発火されるようになる
    • この仕組みの例
    SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
    EXPIRE spring:session:expirations1439245080000 2100
    
    • バックグラウンドタスクは、↑のマッピングを使って、各キーに明示的にアクセスすることで、期限切れになった場合にのみRedisがキーを削除することを保証する
    • キーを明示的に削除しない理由
      • キーの有効期限が切れていないのに誤ってキーを識別してしまうrace conditionが発生する可能性があるため
      • 分散ロックを使用しない限り、有効期限のマッピングの一貫性を確保する方法がない
  • SessionDeletedEventとSessionExpiredEvent
    • SessionDestroyedEventクラスを継承したクラス
    • RedisIndexedSessionRepositoryは、Sessionに紐づくリソースが適切にクリーンアップされることを保証するためにイベントを発火する
      • Sessionが削除されたときはSessionDeletedEventを発火
      • Sessionが期限切れになったときはSessionExpiredEventを発火
    • イベントの発火は、Redis Keyspace eventsをリッスンするSessionMessageListenerを介して行うことができる
      • イベントの発火を有効にする方法
        • 汎用コマンドのRedis Keyspace eventと期限切れイベントを有効にする
        redis-cli config set notify-keyspace-events Egx
        
        • @EnableRedisHttpSessionを使う場合、SessionMessageListenerの管理と必要なRedis Keyspace eventの有効化は自動で行われる
        • セキュリティで保護されたRedis環境で、configコマンドが無効になっている場合はSpring SessionがRedis Keyspace eventを設定できない
        • 自動で設定されることを無効にする場合は、ConfigureRedisAction.NO_OPをBean定義すればOK
        @Bean
        ConfigureRedisAction configureRedisAction() {
        	return ConfigureRedisAction.NO_OP;
        }
        
  • SessionCreatedEvent
    • セッションが作成されるときに、Redisへ送信されるイベント
    • spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32feのようなチャネルIDと一緒に送信される
      • 33fdd1b6-b496-4b33-9f7d-df96679d32feはセッションID
    • イベントのbodyは作成されたセッション
    • RedisIndexedSessionRepositoryがMessageListenerとして登録されている場合(デフォルトで登録されている)は、RedisメッセージをSessionCreatedEventに変換する
  • Redisでセッションを確認する方法
    • redis-cliを使ってセッション情報を確認することができる
    $ redis-cli
    redis 127.0.0.1:6379> keys *
    1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 
    2) "spring:session:expirations:1418772300000" 
    
    • spring:session:sessions:セッションID
    • spring:session:expirations:1418772300000
      • 1418772300000に削除する必要がある全てのセッションIDを格納
    • 各セッションの属性を確認する方法
    redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
    1) "lastAccessedTime"
    2) "creationTime"
    3) "maxInactiveInterval"
    4) "sessionAttr:username"
    redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
    "\xac\xed\x00\x05t\x00\x03rob"
    

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisindexedsessionrepository

daisuzzdaisuzz

CookieSerializerについて

  • セッションCookieの書き込み方法を定義するインターフェース
  • Spring SessionではDefaultCookieSerializerをつかった実装が提供されている
  • CookieSerializerをBean定義すると、@EnableRedisHttpSessionのような設定を使う際に既存の設定を拡張することができる
  • CookieSerializerの拡張例
@Bean
  public CookieSerializer cookieSerializer() {
  	DefaultCookieSerializer serializer = new DefaultCookieSerializer();
  	serializer.setCookieName("JSESSIONID"); 
  	serializer.setCookiePath("/"); 
  	serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); 
  	return serializer;
  }
- Cookieの名前をJSESSIONIDに設定
- Cookieのpath属性をデフォルトのコンテキストルートから`/`に設定
- CookieのDomain属性のパターンを設定

参考

https://docs.spring.io/spring-session/docs/current/reference/html5/#api-cookieserializer

daisuzzdaisuzz

IndexResolverについて

  • セッションに紐づくIndex情報を取得するためのインターフェース
  • 引数で渡したSessionに紐づくIndex情報をMap形式で返す
  • SingleIndexResolver
    • IndexResolverを実装した抽象クラス
    • セッションに紐づく1つのIndex情報を返す
  • PrincipalNameIndexResolver
    • SingleIndexResolverを継承したクラス
    • 以下のどちらかから、principal nameを取得する
      • FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME というセッション属性に格納された値
      • SPRING_SECURITY_CONTEXTというセッション属性に格納されたAuthenticationクラス
daisuzzdaisuzz

Spring Sessionの主なインターフェースと実装クラス

主なインターフェースと実装クラス