Spring SecurityおよびSpring Sessionを使って認証の仕組みを作っていた時に、以下のような疑問を持ったので調べてみたことをメモします。
疑問
「初回認証時にサーバが返してくれるセッションIDをCookieに登録し、次回以降のリクエストでのセッション管理に用いる」ことはよくあることだと思います。
その際、Spring Securityのデフォルト仕様だと
Cookie上で管理されているセッションID...NmUyYzdlMzAtZDRlNi00YTQyLTkxMWEtZDY1MTBjODE1NzIx
DB上に永続化されているセッションID...2b2d43fd-535f-4b31-bbbc-11e34d764bd1
(UUID)
のように、番号形態が異なっています。
SpringSecurityの設定自体は結構簡単なので「正しくCookieにセッションIDを指定すれば、以前の認証情報を持ったまま処理ができる」実装は実現させやすいです。
ただ、内部的な処理として上記のような形態の違う番号ををSpring Sessionはどうやって突き合わせ、認証しているのか気になったので、調べてみました。
結論
CookieSerializerインタフェースのreadCookieValuesメソッドの中でBase64デコードしながら突き合わせている、が結論でした。
- DefaultCookieSerializerというクラスが実装として提供される
- cookieから取得したセッションIDをBase64デコードしつつ永続化先(RDBとかRedisとか)のsessionIdと付き合わせる
といった感じです。
より抜粋↓
@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以外の実装クラスはないんか?と思ったのですが、調べた感じなさそう。
参考
検証した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) }
}