Spring Security Kotlin

認証ライブラリSpring SecurityKotlinJWTOAuth2DSL認証承認セキュリティ

認証ライブラリ

Spring Security Kotlin

概要

Spring Security KotlinはSpring FrameworkのセキュリティフレームワークであるSpring SecurityをKotlin言語で使用するためのDSL(Domain Specific Language)サポートです。JWTトークン認証、OAuth2、SAML、フォーム認証、Basic認証など包括的なセキュリティ機能をKotlinの簡潔で表現力豊かな構文で実装できます。

詳細

Spring Security KotlinはSpring Security 6以降で正式サポートされており、Kotlin DSLを使用してセキュリティ設定を記述できます。従来のJava構文と比較して、Kotlinの関数型プログラミングとDSLの組み合わせによりより読みやすく保守しやすいセキュリティ設定を実現します。JWT認証、OAuth2リソースサーバー、認可ルール、CSRF保護、セッション管理など、エンタープライズレベルのWebアプリケーションに必要なすべてのセキュリティ機能を提供します。

主要機能

  • Kotlin DSL: Kotlinの構文を活用した直感的なセキュリティ設定
  • JWT サポート: JWT トークンベース認証の完全サポート
  • OAuth2 統合: OAuth2クライアント、リソースサーバー、認可サーバーサポート
  • 多様な認証方式: フォーム、Basic、Bearer Token、SAML2など
  • きめ細かい認可制御: URL、メソッド、ドメインレベルでの認可ルール
  • CSRF 保護: クロスサイトリクエストフォージェリ対策
  • セッション管理: セッション固定攻撃対策とセッション制御

サポートする認証方式

  • フォーム認証: Webベースのログインフォーム
  • HTTP Basic: Basic認証ヘッダー
  • Bearer Token: JWT、OAuth2 アクセストークン
  • OAuth2 Login: Google、GitHub、Facebook等の外部プロバイダー
  • SAML2: SAML 2.0 シングルサインオン
  • X.509 証明書: クライアント証明書認証
  • RememberMe: 永続ログイン機能

メリット・デメリット

メリット

  • Kotlin DSL: 型安全で表現力豊かなセキュリティ設定
  • 包括的なセキュリティ: 認証から認可まで完全カバー
  • Spring 統合: Spring Boot、Spring WebFluxとの完全統合
  • 柔軟な設定: YAML、アノテーション、プログラマティック設定対応
  • 豊富なドキュメント: 公式ドキュメントとコミュニティサポート
  • エンタープライズ対応: 大規模システムでの実績
  • アクティブ開発: Spring チームによる継続的な開発とサポート

デメリット

  • 学習コスト: Spring Security とKotlinの両方の知識が必要
  • 設定の複雑さ: 高度な機能を使う場合の設定が複雑
  • パフォーマンス: 多機能による若干のオーバーヘッド
  • Kotlin 限定: Kotlin専用のDSL(Javaでも従来通り使用可能)
  • バージョン依存: Spring Boot とのバージョン互換性管理が必要

参考ページ

書き方の例

基本設定

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.security:spring-security-config")
    implementation("io.jsonwebtoken:jjwt-api:0.11.5")
    implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
    implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
}

基本的なセキュリティ設定

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/login", permitAll)
                authorize("/public/**", permitAll)
                authorize(anyRequest, authenticated)
            }
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                failureUrl = "/login?error=true"
            }
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/login"
                deleteCookies("JSESSIONID")
            }
            csrf { 
                disable() 
            }
        }
        return http.build()
    }
}

JWT認証設定

import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets

@Configuration
@EnableWebSecurity
class JwtSecurityConfig {

    private val jwtSecret = "your-256-bit-secret-key-for-jwt-tokens-here"

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
            authorizeHttpRequests {
                authorize("/api/auth/**", permitAll)
                authorize("/api/public/**", permitAll)
                authorize("/api/**", authenticated)
                authorize(anyRequest, permitAll)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = jwtDecoder()
                }
            }
            csrf { 
                disable() 
            }
        }
        return http.build()
    }

    @Bean
    fun jwtDecoder(): JwtDecoder {
        val secretKey: SecretKey = SecretKeySpec(
            jwtSecret.toByteArray(StandardCharsets.UTF_8), 
            "HmacSHA256"
        )
        return NimbusJwtDecoder.withSecretKey(secretKey).build()
    }
}

OAuth2 ログイン設定

@Configuration
@EnableWebSecurity
class OAuth2SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/", permitAll)
                authorize("/login", permitAll)
                authorize("/error", permitAll)
                authorize(anyRequest, authenticated)
            }
            oauth2Login {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                failureUrl = "/login?error=true"
                userInfoEndpoint {
                    userService = customOAuth2UserService()
                }
            }
            logout {
                logoutSuccessUrl = "/"
                invalidateHttpSession = true
                deleteCookies("JSESSIONID")
            }
        }
        return http.build()
    }

    @Bean
    fun customOAuth2UserService(): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        return CustomOAuth2UserService()
    }
}

// カスタムOAuth2ユーザーサービス
class CustomOAuth2UserService : DefaultOAuth2UserService() {
    
    override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
        val oAuth2User = super.loadUser(userRequest)
        
        // ユーザー情報をデータベースに保存またはマッピング
        processOAuthPostLogin(oAuth2User, userRequest.clientRegistration.registrationId)
        
        return oAuth2User
    }
    
    private fun processOAuthPostLogin(oAuth2User: OAuth2User, provider: String) {
        val email = oAuth2User.getAttribute<String>("email")
        val name = oAuth2User.getAttribute<String>("name")
        
        // ユーザー情報の処理ロジック
        println("OAuth2 User: $name ($email) from $provider")
    }
}

メソッドレベルセキュリティ

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.access.prepost.PostAuthorize
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.web.bind.annotation.*

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
class MethodSecurityConfig

@RestController
@RequestMapping("/api/admin")
class AdminController {

    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    fun getAllUsers(): List<User> {
        // 管理者のみアクセス可能
        return userService.findAll()
    }

    @GetMapping("/user/{id}")
    @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(authentication.name, #id)")
    fun getUserById(@PathVariable id: Long): User {
        // 管理者または本人のみアクセス可能
        return userService.findById(id)
    }

    @PostMapping("/user")
    @PreAuthorize("hasAuthority('USER_CREATE')")
    fun createUser(@RequestBody user: User): User {
        return userService.save(user)
    }

    @DeleteMapping("/user/{id}")
    @PreAuthorize("hasRole('SUPER_ADMIN')")
    fun deleteUser(@PathVariable id: Long) {
        userService.deleteById(id)
    }
}

複数の認証方式設定

@Configuration
@EnableWebSecurity
class MultiAuthSecurityConfig {

    @Bean
    @Order(1)
    fun apiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher("/api/**")
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
            authorizeHttpRequests {
                authorize("/api/auth/**", permitAll)
                authorize("/api/**", authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = jwtDecoder()
                }
            }
            httpBasic { }
            csrf { disable() }
        }
        return http.build()
    }

    @Bean
    @Order(2)
    fun webSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/login", permitAll)
                authorize("/register", permitAll)
                authorize("/public/**", permitAll)
                authorize(anyRequest, authenticated)
            }
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
            }
            oauth2Login {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
            }
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/login"
            }
        }
        return http.build()
    }
}

カスタム認証プロバイダー

import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class CustomAuthenticationProvider(
    private val userService: UserService,
    private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {

    override fun authenticate(authentication: Authentication): Authentication {
        val username = authentication.name
        val password = authentication.credentials.toString()

        val user = userService.findByUsername(username)
            ?: throw BadCredentialsException("Invalid username or password")

        if (!passwordEncoder.matches(password, user.password)) {
            throw BadCredentialsException("Invalid username or password")
        }

        if (!user.enabled) {
            throw BadCredentialsException("Account is disabled")
        }

        val authorities = user.roles.map { SimpleGrantedAuthority("ROLE_$it") }

        return UsernamePasswordAuthenticationToken(username, password, authorities)
    }

    override fun supports(authentication: Class<*>): Boolean {
        return authentication == UsernamePasswordAuthenticationToken::class.java
    }
}

// カスタムプロバイダーの設定
@Configuration
class AuthConfig(
    private val customAuthenticationProvider: CustomAuthenticationProvider
) {

    @Bean
    fun authenticationManager(): AuthenticationManager {
        return ProviderManager(customAuthenticationProvider)
    }
}

リアクティブセキュリティ(WebFlux)

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain

@Configuration
@EnableWebFluxSecurity
class ReactiveSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            authorizeExchange {
                authorize("/api/public/**", permitAll)
                authorize("/api/**", authenticated)
                authorize(anyExchange, permitAll)
            }
            oauth2ResourceServer {
                jwt { }
            }
            csrf { disable() }
        }
    }
}

セキュリティイベントハンドリング

import org.springframework.context.event.EventListener
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
import org.springframework.security.authorization.event.AuthorizationDeniedEvent
import org.springframework.stereotype.Component

@Component
class SecurityEventListener {

    @EventListener
    fun handleAuthenticationSuccess(event: AuthenticationSuccessEvent) {
        val username = event.authentication.name
        println("Successful authentication for user: $username")
        
        // ログイン成功時の処理(ログ記録、統計更新など)
        auditService.logSuccessfulLogin(username)
    }

    @EventListener
    fun handleAuthenticationFailure(event: AbstractAuthenticationFailureEvent) {
        val username = event.authentication.name
        val exception = event.exception
        println("Failed authentication for user: $username, reason: ${exception.message}")
        
        // ログイン失敗時の処理(セキュリティログ、アカウントロック等)
        securityService.handleFailedLogin(username, exception)
    }

    @EventListener
    fun handleAuthorizationDenied(event: AuthorizationDeniedEvent<*>) {
        val username = event.authentication?.name ?: "anonymous"
        println("Access denied for user: $username")
        
        // アクセス拒否時の処理
        auditService.logAccessDenied(username, event.authorizationDecision.toString())
    }
}

CSRF保護とカスタムヘッダー

@Configuration
@EnableWebSecurity
class CsrfSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/api/**", authenticated)
                authorize(anyRequest, permitAll)
            }
            csrf {
                csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
                ignoringRequestMatchers("/api/webhook/**")
            }
            headers {
                frameOptions {
                    deny()
                }
                contentTypeOptions { }
                httpStrictTransportSecurity {
                    maxAgeInSeconds = 31536000
                    includeSubDomains = true
                }
                referrerPolicy {
                    policy = ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
                }
            }
        }
        return http.build()
    }
}