Kotlin の設定

Spring Security Kotlin 構成は、Spring Security 5.3 から利用可能になりました。これにより、ユーザーはネイティブ Kotlin DSL を使用して Spring Security を構成できるようになります。

Spring Security は、Spring Security Kotlin 構成の使用箇所を示すためにサンプルアプリケーション [GitHub] (英語) を提供します。

HttpSecurity

Spring Security は、すべてのユーザーに認証を要求することをどのように認識しますか? Spring Security は、フォームベースの認証をサポートすることをどのように認識していますか? バックグラウンドで呼び出されている構成クラス(SecurityFilterChain と呼ばれる)があります。これは、次のデフォルトの実装で構成されています。

import org.springframework.security.config.annotation.web.invoke

@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        httpBasic { }
    }
    return http.build()
}
IDE がメソッドを常に自動インポートするとは限らず、コンパイルの問題が発生する可能性があるため、クラスで Kotlin DSL を有効にするには、必ず org.springframework.security.config.annotation.web.invoke 関数をインポートしてください。

デフォルトの構成(前の例に示されています):

  • アプリケーションへのリクエストでは、ユーザーの認証が必要であることを保証します

  • ユーザーがフォームベースのログインで認証できるようにします

  • ユーザーが HTTP 基本認証で認証できるようにします

この構成は XML 名前空間の構成と類似していることに注意してください。

<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

複数の HttpSecurity インスタンス

特定の領域に異なる保護が必要なアプリケーションのセキュリティを効果的に管理するには、securityMatcher DSL メソッドと並行して複数のフィルターチェーンを使用します。このアプローチにより、アプリケーションの特定の部分に合わせて個別のセキュリティ構成を定義し、アプリケーション全体のセキュリティと制御を強化できます。

XML で複数の <http> ブロックを持つことができるのと同じように、複数の HttpSecurity インスタンスを構成できます。重要なのは、複数の SecurityFilterChain または @Bean を登録することです。次の例では、/api/ で始まる URL に対して異なる構成が使用されています。

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class MultiHttpSecurityConfig {
    @Bean                                                            (1)
    open fun userDetailsService(): UserDetailsService {
        val users = User.withDefaultPasswordEncoder()
        val manager = InMemoryUserDetailsManager()
        manager.createUser(users.username("user").password("password").roles("USER").build())
        manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build())
        return manager
    }

    @Bean
    @Order(1)                                                        (2)
    open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher("/api/**")                               (3)
            authorizeHttpRequests {
                authorize(anyRequest, hasRole("ADMIN"))
            }
            httpBasic { }
        }
        return http.build()
    }

    @Bean                                                            (4)
    open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            formLogin { }
        }
        return http.build()
    }
}
1 通常どおり認証を構成します。
2@Order を含む SecurityFilterChain のインスタンスを作成して、どの SecurityFilterChain を最初に考慮するかを指定します。
3http.securityMatcher() では、この HttpSecurity は /api/ で始まる URL にのみ適用されることが規定されています。
4SecurityFilterChain の別のインスタンスを作成します。URL が /api/ で始まらない場合は、この構成が使用されます。この構成は、1 の後に @Order 値があるため、apiFilterChain の後に考慮されます (@Order はデフォルトで最後にはなりません)。

securityMatcher または requestMatchers の選択

よくある質問は次のとおりです。

リクエストの認可に使用される http.securityMatcher() メソッドと requestMatchers() (つまり、http.authorizeHttpRequests() 内部) の違いは何ですか ?

この質問に答えるには、SecurityFilterChain の構築に使用される各 HttpSecurity インスタンスに、受信リクエストに一致する RequestMatcher が含まれていることを理解すると役立ちます。リクエストが優先度の高い SecurityFilterChain (例: @Order(1)) と一致しない場合、リクエストは優先度の低いフィルターチェーン (例: @Order なし) に対して試行されます。

複数のフィルターチェーンのマッチングロジックは、FilterChainProxy によって実行されます。

デフォルトの RequestMatcher はすべてのリクエストに一致し、Spring Security がデフォルトですべてのリクエストを保護するようにします。

securityMatcher を指定すると、このデフォルトが上書きされます。

特定のリクエストに一致するフィルターチェーンがない場合、そのリクエストは Spring Security によって保護されません

次の例は、/secured/ で始まるリクエストのみを保護する単一のフィルターチェーンを示しています。

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class PartialSecurityConfig {
	@Bean
	open fun userDetailsService(): UserDetailsService {
		// ...
	}

	@Bean
	open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			securityMatcher("/secured/**")                             (1)
			authorizeHttpRequests {
				authorize("/secured/user", hasRole("USER"))            (2)
				authorize("/secured/admin", hasRole("ADMIN"))          (3)
				authorize(anyRequest, authenticated)                   (4)
			}
			httpBasic { }
			formLogin { }
		}
		return http.build()
	}
}
1/secured/ で始まるリクエストは保護されますが、その他のリクエストは保護されません。
2/secured/user へのリクエストには ROLE_USER 権限が必要です。
3/secured/admin へのリクエストには ROLE_ADMIN 権限が必要です。
4 その他のリクエスト (/secured/other など) では、認証されたユーザーのみが必要です。

前の例で示したように、アプリケーション全体が保護されるように、securityMatcher を指定しない SecurityFilterChain を提供することをお勧めします

requestMatchers メソッドは個々の認可ルールにのみ適用されることに注意してください。そこにリストされている各リクエストは、SecurityFilterChain の作成に使用されたこの特定の HttpSecurity インスタンスの全体的な securityMatcher とも一致する必要があります。この例で anyRequest() を使用すると、この特定の SecurityFilterChain ( /secured/ で始まる必要があります) 内の他のすべてのリクエストと一致します。

requestMatchers の詳細については、HttpServletRequests を認証するを参照してください。

SecurityFilterChain エンドポイント

SecurityFilterChain のいくつかのフィルターは、http.formLogin() によってセットアップされ、POST /login エンドポイントを提供する UsernamePasswordAuthenticationFilter など、エンドポイントを直接提供します。上記の例では、/login エンドポイントは http.securityMatcher("/secured/**") と一致しないため、そのアプリケーションには GET /login または POST /login エンドポイントがありません。このようなリクエストは 404 Not Found を返します。これはユーザーにとってしばしば驚きです。

http.securityMatcher() を指定すると、その SecurityFilterChain に一致するリクエストに影響します。ただし、フィルターチェーンによって提供されるエンドポイントには自動的に影響しません。このような場合は、フィルターチェーンが提供するエンドポイントの URL をカスタマイズする必要がある場合があります。

次の例は、/secured/ で始まるリクエストを保護し、他のすべてのリクエストを拒否するとともに、SecurityFilterChain によって提供されるエンドポイントをカスタマイズする構成を示しています。

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecuredSecurityConfig {
	@Bean
	open fun userDetailsService(): UserDetailsService {
		// ...
	}

	@Bean
	@Order(1)
	open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			securityMatcher("/secured/**")                             (1)
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)                   (2)
			}
			formLogin {                                                (3)
                loginPage = "/secured/login"
                loginProcessingUrl = "/secured/login"
                permitAll = true
			}
			logout {                                                   (4)
                logoutUrl = "/secured/logout"
                logoutSuccessUrl = "/secured/login?logout"
                permitAll = true
			}
		}
		return http.build()
	}

	@Bean
    open fun defaultFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, denyAll)                         (5)
            }
        }
        return http.build()
    }
}
1/secured/ で始まるリクエストは、このフィルターチェーンによって保護されます。
2/secured/ で始まるリクエストには認証されたユーザーが必要です。
3 フォームログインをカスタマイズして、URL の先頭に /secured/ を付けます。
4 ログアウトをカスタマイズして、URL のプレフィックスに /secured/ を付けます。
5 その他のリクエストはすべて拒否されます。

この例では、ログインページとログアウトページをカスタマイズし、Spring Security の生成されたページを無効にします。GET /secured/login と GET /secured/logout には独自のカスタムエンドポイントを提供する必要があります。Spring Security は引き続き POST /secured/login と POST /secured/logout エンドポイントを提供することに注意してください。

実世界の例

次の例は、これらすべての要素を組み合わせた、もう少し現実的な構成を示しています。

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class BankingSecurityConfig {
    @Bean                                                              (1)
    open fun userDetailsService(): UserDetailsService {
        val users = User.withDefaultPasswordEncoder()
        val manager = InMemoryUserDetailsManager()
        manager.createUser(users.username("user1").password("password").roles("USER", "VIEW_BALANCE").build())
        manager.createUser(users.username("user2").password("password").roles("USER").build())
        manager.createUser(users.username("admin").password("password").roles("ADMIN").build())
        return manager
    }

    @Bean
    @Order(1)                                                          (2)
    open fun approvalsSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val approvalsPaths = arrayOf("/accounts/approvals/**", "/loans/approvals/**", "/credit-cards/approvals/**")
        http {
            securityMatcher(*approvalsPaths)
            authorizeHttpRequests {
				authorize(anyRequest, hasRole("ADMIN"))
            }
            httpBasic { }
        }
        return http.build()
    }

    @Bean
    @Order(2)                                                          (3)
	open fun bankingSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val bankingPaths = arrayOf("/accounts/**", "/loans/**", "/credit-cards/**", "/balances/**")
		val viewBalancePaths = arrayOf("/balances/**")
        http {
            securityMatcher(*bankingPaths)
            authorizeHttpRequests {
                authorize(viewBalancePaths, hasRole("VIEW_BALANCE"))
				authorize(anyRequest, hasRole("USER"))
            }
        }
        return http.build()
    }

    @Bean                                                              (4)
	open fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val allowedPaths = arrayOf("/", "/user-login", "/user-logout", "/notices", "/contact", "/register")
        http {
            authorizeHttpRequests {
                authorize(allowedPaths, permitAll)
				authorize(anyRequest, authenticated)
            }
			formLogin {
                loginPage = "/user-login"
                loginProcessingUrl = "/user-login"
			}
			logout {
                logoutUrl = "/user-logout"
                logoutSuccessUrl = "/?logout"
			}
        }
        return http.build()
    }
}
1 まず認証設定を構成します。
2@Order(1) を使用して SecurityFilterChain インスタンスを定義します。つまり、このフィルターチェーンの優先度が最も高くなります。このフィルターチェーンは、/accounts/approvals//loans/approvals/、または /credit-cards/approvals/ で始まるリクエストにのみ適用されます。このフィルターチェーンへのリクエストには ROLE_ADMIN 権限が必要であり、HTTP 基本認証が許可されます。
3 次に、2 番目とみなされる @Order(2) を使用して、別の SecurityFilterChain インスタンスを作成します。このフィルターチェーンは、/accounts//loans//credit-cards/ または /balances/ で始まるリクエストにのみ適用されます。このフィルターチェーンは 2 番目であるため、/approvals/ を含むすべてのリクエストは前のフィルターチェーンと一致し、このフィルターチェーンとは一致しないことに注意してください。このフィルターチェーンへのリクエストには、ROLE_USER 権限が必要です。このフィルターチェーンは、次の (デフォルトの) フィルターチェーンにその構成が含まれているため、認証を定義しません。
4 最後に、@Order アノテーションなしで追加の SecurityFilterChain インスタンスを作成します。この構成は、他のフィルターチェーンでカバーされていないリクエストを処理し、最後に処理されます (@Order がない場合は最後にデフォルトで処理されます)。//user-login/user-logout/notices/contact/register に一致するリクエストは、認証なしでアクセスできます。その他のリクエストでは、他のフィルターチェーンによって明示的に許可または保護されていない URL にアクセスするには、ユーザーの認証が必要です。

モジュラー HttpSecurityDsl 構成

多くのユーザーは、Spring Security の設定を一元管理することを好み、単一の SecurityFilterChain インスタンスで設定することを選択します。しかし、設定をモジュール化したい場合もあります。これは、以下の方法で実現できます。

Spring Security Kotlin Dsl (HttpSecurityDsl) は HttpSecurity を使用するため、すべての Java モジュラー Bean のカスタマイズモジュラー HttpSecurity 構成の前に適用されます。

HttpSecurityDsl.() → ユニットビーンズ

セキュリティ構成をモジュール化したい場合は、HttpSecurityDsl.() → Unit Bean にロジックを配置できます。例: 次の構成では、すべての HttpSecurityDsl インスタンスが次のように構成されます。

@Bean
fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit {
    return {
        headers {
            contentSecurityPolicy {
                (1)
                policyDirectives = "object-src 'none'"
            }
        }
        (2)
        redirectToHttps { }
    }
}

トップレベルのセキュリティ DSL Bean

セキュリティ構成をさらにモジュール化したい場合は、Spring Security によってトップレベルのセキュリティ Dsl Bean が自動的に適用されます。

トップレベルのセキュリティ Dsl は、public HttpSecurityDsl.*(<Dsl>) に一致する任意のクラス Dsl クラスとして要約できます。これは、HttpSecurityDsl の public メソッドへの単一の引数となる任意のセキュリティ Dsl に相当します。

いくつかの例を挙げて説明しましょう。ContentTypeOptionsDsl.() → Unit が Bean として公開された場合、HeadersDsl#contentTypeOptions(ContentTypeOptionsDsl.() → Unit) の引数であり、HttpSecurityDsl で定義されたメソッドの引数ではないため、自動的には適用されません。一方、HeadersDsl.() → Unit が Bean として公開された場合、HttpSecurityDsl.headers(HeadersDsl.() → Unit) の引数であるため、自動的に適用されます。

例: 次の構成では、すべての HttpSecurityDsl インスタンスが次のように構成されていることを確認します。

@Bean
fun headersSecurity(): HeadersDsl.() -> Unit {
    return {
        contentSecurityPolicy {
            (1)
            policyDirectives = "object-src 'none'"
        }
    }
}
1 コンテンツセキュリティポリシーを object-src 'none' に設定する

Dsl Bean オーダー

まず、Kotlin Dsl は HttpSecurity Bean を使用するため、すべてのモジュラー HttpSecurity 構成が適用されます。

次に、各 HttpSecurityDsl.() → ユニットビーンズObjectProvider#orderedStream() (Javadoc) を使用して適用されます。つまり、複数の HttpSecurity.() → Unit Bean がある場合、Bean 定義に @Order (Javadoc) アノテーションを追加して順序を制御できます。

次に、すべてのトップレベルのセキュリティ DSL Bean 型が検索され、それぞれが ObjectProvider#orderedStream() を使用して適用されます。異なる型のトップレベルセキュリティ Bean(例: HeadersDsl.() → Unit と HttpsRedirectDsl.() → Unit)がある場合、各 Dsl 型の呼び出し順序は未定義です。ただし、同じトップレベルセキュリティ Bean 型の各インスタンスの呼び出し順序は ObjectProvider#orderedStream() によって定義され、Bean 定義の @Order を使用して制御できます。

最後に、HttpSecurityDsl Bean が Bean として注入されます。*Dsl.() → Unit Bean はすべて、HttpSecurityDsl Bean が作成される前に適用されます。これにより、*Dsl.() → Unit Bean によって提供されるカスタマイズをオーバーライドできます。

順序を示す例を以下に示します。

// All of the Java Modular Configuration is applied first (1)

@Bean (5)
fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
    }
    return http.build()
}

@Bean
@Order(Ordered.LOWEST_PRECEDENCE)  (3)
fun userAuthorization(): HttpSecurityDsl.() -> Unit {
    return {
        authorizeHttpRequests {
            authorize("/users/**", hasRole("USER"))
        }
    }
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (2)
fun adminAuthorization(): HttpSecurityDsl.() -> Unit {
    return {
        authorizeHttpRequests {
            authorize("/admins/**", hasRole("ADMIN"))
        }
    }
}

(4)

@Bean
fun contentSecurityPolicy(): HeadersDsl.() -> Unit {
    return {
        contentSecurityPolicy {
            policyDirectives = "object-src 'none'"
        }
    }
}

@Bean
fun contentTypeOptions(): HeadersDsl.() -> Unit {
    return {
        contentTypeOptions { }
    }
}

@Bean
fun httpsRedirect(): HttpsRedirectDsl.() -> Unit {
    return { }
}
1Kotlin Dsl は HttpSecurity Bean を使用するため、すべてのモジュラー HttpSecurity 構成が適用されます。
2 すべての HttpSecurity.() → Unit インスタンスが適用されます。adminAuthorization (Bean)は @Order が最も高いため、最初に適用されます。HttpSecurity.() → Unit Bean に @Order アノテーションがない場合、または @Order アノテーションの値が同じ場合、HttpSecurity.() → Unit インスタンスの適用順序は未定義です。
3userAuthorization は HttpSecurity.() → Unit のインスタンスであるため次に適用されます
4*Dsl.() → Unit 型の順序は未定義です。この例では、contentSecurityPolicycontentTypeOptionshttpsRedirect の順序は未定義です。@Order(Ordered.HIGHEST_PRECEDENCE) が contentTypeOptions に追加された場合、contentTypeOptions が contentSecurityPolicy の前(同じ型)にあることはわかりますが、httpsRedirect が HeadersDsl.() → Unit Bean の前か後かはわかりません。
5 すべての *Dsl.() → Unit Bean が適用された後、HttpSecurityDsl が Bean として渡されます。