HIRO Tracks

ソフトウェアエンジニアが日々学んだ知識を発信します。

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

これはなに?

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

疑問

「初回認証時にサーバが返してくれるセッション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)
    }
}