Spring

Spring Sessionがcookieの値からsessionを特定する処理はどこにある?【DefaultCookieSerializer】

2023年12月2日

Spring SecurityおよびSpring Sessionを使って認証の仕組みを作っていた時に、以下のような疑問を持ったので調べてみたことをメモします。

ITエンジニア6年目の山根です。X(Twitter)やってます。自己紹介,お問い合わせはこちらまで!

疑問

「初回認証時にサーバが返してくれるセッションIDをCookieに登録し、次回以降のリクエストでのセッション管理に用いる」ことはよくあることだと思います。

その際、Spring Securityのデフォルト仕様だと

Cookie上で管理されているセッションID...NmUyYzdlMzAtZDRlNi00YTQyLTkxMWEtZDY1MTBjODE1NzIx

DB上に永続化されているセッションID...2b2d43fd-535f-4b31-bbbc-11e34d764bd1(UUID)

↑Cookie上で管理されているセッションID
↑DB上に永続化されているセッションID

のように、番号形態が異なっています。

SpringSecurityの設定自体は結構簡単なので「正しくCookieにセッションIDを指定すれば、以前の認証情報を持ったまま処理ができる」実装は実現させやすいです。

ただ、内部的な処理として上記のような形態の違う番号ををSpring Sessionはどうやって突き合わせ、認証しているのか気になったので、調べてみました。

結論

CookieSerializerインタフェースのreadCookieValuesメソッドの中でBase64デコードしながら突き合わせている、が結論でした。

  • DefaultCookieSerializerというクラスが実装として提供される
  • cookieから取得したセッションIDをBase64デコードしつつ永続化先(RDBとかRedisとか)のsessionIdと付き合わせる

といった感じです。

github.com

より抜粋↓

	@Override
	public List<String> readCookieValues(HttpServletRequest request) {
		Cookie[] cookies = request.getCookies();
		List<String> matchingCookieValues = new ArrayList<>();
		if (cookies != null) {
			for (Cookie cookie : cookies) {
				if (this.cookieName.equals(cookie.getName())) {
					String sessionId = (this.useBase64Encoding ? base64Decode(cookie.getValue()) : cookie.getValue());
					if (sessionId == null) {
						continue;
					}
					if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
						sessionId = sessionId.substring(0, sessionId.length() - this.jvmRoute.length());
					}
					matchingCookieValues.add(sessionId);
				}
			}
		}
		return matchingCookieValues;
	}

DefaultCookieSerializer以外の実装クラスはないんか?と思ったのですが、調べた感じなさそう。

spring.pleiades.io

参考

検証したbuild.gralde.ktsのdependencies

dependencies { 
    implementation("org.springframework.boot:spring-boot-starter-security:3.0.7")             
    implementation("org.springframework.boot:spring-boot-starter-web:3.1.0") 
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2") 
    implementation("org.jetbrains.kotlin:kotlin-reflect") 
    implementation("org.springframework.session:spring-session-core:3.0.0") 
    implementation("com.h2database:h2:2.1.214") 
    implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.0.4") 
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc:3.0.4") 
    implementation("org.springframework.session:spring-session-jdbc:3.0.0") 
    testImplementation("org.springframework.boot:spring-boot-starter-test:3.1.0") 
    testImplementation("org.springframework.security:spring-security-test:6.0.2") 
}

検証したSpring Security設定クラス

package com.example.demo import org.springframework.context.annotation.Bean 
import org.springframework.context.annotation.Configuration 
import org.springframework.security.authentication.AuthenticationManager 
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration 
import org.springframework.security.config.annotation.web.builders.HttpSecurity 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 
import org.springframework.security.core.session.SessionRegistry 
import org.springframework.security.core.session.SessionRegistryImpl 
import org.springframework.security.core.userdetails.UserDetailsService 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 
import org.springframework.security.crypto.password.PasswordEncoder 
import org.springframework.security.web.AuthenticationEntryPoint 
import org.springframework.security.web.SecurityFilterChain 
import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher 

@EnableWebSecurity 
@Configuration 
class AppSecurityConfig { 

    @Bean 
   fun config(http: HttpSecurity): SecurityFilterChain { 
       http.authorizeHttpRequests { 
           it.requestMatchers( antMatcher("/h2-console/**"), antMatcher("/error/**"), antMatcher("/login") )
             .permitAll() 
             .anyRequest()
             .authenticated() 
           }.exceptionHandling { exceptionHandling -> exceptionHandling .authenticationEntryPoint(unAuthorizedHandler())// 未認証の場合は401 
       }.csrf { config -> config.ignoringRequestMatchers(antMatcher("/h2-console/**"), antMatcher("/login")) // h2-console通信にはPOSTある + POSTだとCSRFかかるので停止 }
       .headers { config -> config.frameOptions { config -> config.sameOrigin() // h2console上でiframe使えないので修正 } 
       } http.sessionManagement { config -> config.maximumSessions(1).sessionRegistry(sessionRegistry()) // cookieの"SESSION"をBase64デコードしてDBのsessionIdと突き合わせ 
       } return http.build() } 
       
   @Bean 
   fun unAuthorizedHandler(): AuthenticationEntryPoint { 
       return UnAuthorizedHandler() 
   } 
   
   @Bean fun sessionRegistry(): SessionRegistry { 
       return SessionRegistryImpl() 
   } 
       
   @Bean 
   fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration?): AuthenticationManager? { 
       return authenticationConfiguration?.authenticationManager 
   } 
   
   @Bean 
   fun myUserDetailsService(): UserDetailsService { 
       return MyUserDetailsService() 
   } 
   
   @Bean fun passwordEncoder(): PasswordEncoder { return BCryptPasswordEncoder(8) } 
 }

-Spring