Spring MVC 統合
Spring Security は、Spring MVC とのオプションの統合を多数提供します。このセクションでは、統合についてさらに詳しく説明します。
@EnableWebMvcSecurity
Spring Security 4.0 の時点で、 |
Spring Security と Spring MVC の統合を有効にするには、構成に @EnableWebSecurity
アノテーションを追加します。
Spring Security は、Spring MVC の |
MvcRequestMatcher
Spring Security は、Spring MVC が URL で MvcRequestMatcher
と一致する方法との緊密な統合を提供します。これは、セキュリティルールがリクエストの処理に使用されるロジックと一致することを確認できます。
MvcRequestMatcher
を使用するには、Spring Security 構成を DispatcherServlet
と同じ ApplicationContext
に配置する必要があります。これが必要なのは、Spring Security の MvcRequestMatcher
は、mvcHandlerMappingIntrospector
という名前の HandlerMappingIntrospector
Bean が、マッチングの実行に使用される Spring MVC 構成によって登録されることを想定しているためです。
web.xml
ファイルの場合、これは、構成を DispatcherServlet.xml
に配置する必要があることを意味します。
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Load from the ContextLoaderListener -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
次の WebSecurityConfiguration
は、DispatcherServlet
の ApplicationContext
に配置されます。
Java
Kotlin
public class SecurityInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { RootConfiguration.class,
WebMvcConfiguration.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
class SecurityInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(
RootConfiguration::class.java,
WebMvcConfiguration::class.java
)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
|
次のようにマップされているコントローラーを考えます。
Java
Kotlin
@RequestMapping("/admin")
public String admin() {
// ...
}
@RequestMapping("/admin")
fun admin(): String {
// ...
}
このコントローラーメソッドへのアクセスを管理者ユーザーに制限するには、HttpServletRequest
で次のように照合することにより、認可ルールを提供できます。
Java
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin").hasRole("ADMIN")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/admin", hasRole("ADMIN"))
}
}
return http.build()
}
次のリストは、XML でも同じことを行います。
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
どちらの構成でも、/admin
URL では、認証されたユーザーが管理者ユーザーである必要があります。ただし、Spring MVC 構成によっては、/admin.html
URL も admin()
メソッドにマップされます。さらに、Spring MVC 構成によっては、/admin
URL も admin()
メソッドにマップされます。
問題は、セキュリティルールが /admin
のみを保護することです。Spring MVC のすべての順列にルールを追加することもできますが、これは非常に冗長で面倒です。
さいわい、requestMatchers
DSL メソッドを使用する場合、Spring Security はクラスパスで Spring MVC が使用可能であることを検出すると、自動的に MvcRequestMatcher
を作成します。Spring MVC を使用して URL を照合することにより、Spring MVC が照合するのと同じ URL を保護します。
Spring MVC を使用する場合の一般的な要件の 1 つは、サーブレットパスプロパティを指定することです。これにより、MvcRequestMatcher.Builder
を使用して、同じサーブレットパスを共有する複数の MvcRequestMatcher
インスタンスを作成できます。
Java
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val mvcMatcherBuilder = MvcRequestMatcher.Builder(introspector)
http {
authorizeHttpRequests {
authorize(mvcMatcherBuilder.pattern("/admin"), hasRole("ADMIN"))
authorize(mvcMatcherBuilder.pattern("/user"), hasRole("USER"))
}
}
return http.build()
}
次の XML にも同じ効果があります。
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
@AuthenticationPrincipal
Spring Security は AuthenticationPrincipalArgumentResolver
を提供します。これは、Spring MVC 引数の現在の Authentication.getPrincipal()
を自動的に解決できます。@EnableWebSecurity
を使用すると、これが Spring MVC 構成に自動的に追加されます。XML ベースの構成を使用する場合は、これを自分で追加する必要があります。
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
AuthenticationPrincipalArgumentResolver
を適切に構成すると、Spring MVC レイヤーで Spring Security から完全に切り離すことができます。
カスタム UserDetailsService
が UserDetails
と独自の CustomUser
Object
を実装する Object
を返す状況を考えてみます。現在認証されているユーザーの CustomUser
には、次のコードを使用してアクセスできます。
Java
Kotlin
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();
// .. find messages for this user and return them ...
}
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(): ModelAndView {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
val custom: CustomUser? = if (authentication as CustomUser == null) null else authentication.principal
// .. find messages for this user and return them ...
}
Spring Security 3.2 では、アノテーションを追加することで、引数をより直接的に解決できます。
Java
Kotlin
import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {
// .. find messages for this user and return them ...
}
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@AuthenticationPrincipal customUser: CustomUser?): ModelAndView {
// .. find messages for this user and return them ...
}
場合によっては、何らかの方法でプリンシパルを変換する必要があります。例: CustomUser
を最終版にする必要がある場合、拡張できませんでした。この状況では、UserDetailsService
は、UserDetails
を実装し、CustomUser
にアクセスするための getCustomUser
という名前のメソッドを提供する Object
を返す場合があります。
Java
Kotlin
public class CustomUserUserDetails extends User {
// ...
public CustomUser getCustomUser() {
return customUser;
}
}
class CustomUserUserDetails(
username: String?,
password: String?,
authorities: MutableCollection<out GrantedAuthority>?
) : User(username, password, authorities) {
// ...
val customUser: CustomUser? = null
}
次に、ルートオブジェクトとして Authentication.getPrincipal()
を使用する SpEL 式を使用して、CustomUser
にアクセスできます。
Java
Kotlin
import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {
// .. find messages for this user and return them ...
}
import org.springframework.security.core.annotation.AuthenticationPrincipal
// ...
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") customUser: CustomUser?): ModelAndView {
// .. find messages for this user and return them ...
}
SpEL 式で Bean を参照することもできます。例: JPA を使用してユーザーを管理している場合、および現在のユーザーのプロパティを変更して保存する場合は、次を使用できます。
Java
Kotlin
import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
@RequestParam String firstName) {
// change the firstName on an attached instance which will be persisted to the database
attachedCustomUser.setFirstName(firstName);
// ...
}
import org.springframework.security.core.annotation.AuthenticationPrincipal
// ...
@PutMapping("/users/self")
open fun updateName(
@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") attachedCustomUser: CustomUser,
@RequestParam firstName: String?
): ModelAndView {
// change the firstName on an attached instance which will be persisted to the database
attachedCustomUser.setFirstName(firstName)
// ...
}
@AuthenticationPrincipal
を独自のアノテーションのメタアノテーションにすることで、Spring Security への依存をさらに取り除くことができます。次の例は、@CurrentUser
という名前のアノテーションでこれを行う方法を示しています。
Spring Security への依存を取り除くために、 |
Java
Kotlin
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal
annotation class CurrentUser
Spring Security への依存関係を単一のファイルに分離しました。@CurrentUser
が指定されたため、これを使用して、現在認証されているユーザーの CustomUser
を解決するようにシグナルを送ることができます。
Java
Kotlin
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {
// .. find messages for this user and return them ...
}
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView {
// .. find messages for this user and return them ...
}
メタアノテーションになると、パラメーター化も利用できるようになります。
たとえば、プリンシパルとして JWT があり、どのクレームを取得するかを指定したい場合を考えてみましょう。メタアノテーションとして、次のようにします。
Java
Kotlin
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['sub']")
public @interface CurrentUser {}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal(expression = "claims['sub']")
annotation class CurrentUser
これはすでにかなり強力です。しかし、これも sub
クレームの取得に限定されています。
これをより柔軟にするには、まず AnnotationTemplateExpressionDefaults
Bean を次のように公開します。
Java
Kotlin
XML
@Bean
public AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDeafults();
}
@Bean
fun templateDefaults(): AnnotationTemplateExpressionDefaults {
return AnnotationTemplateExpressionDeafults()
}
<b:bean name="annotationExpressionTemplateDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/>
そして、次のように @CurrentUser
にパラメーターを指定できます。
Java
Kotlin
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['{claim}']")
public @interface CurrentUser {
String claim() default 'sub';
}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal(expression = "claims['{claim}']")
annotation class CurrentUser(val claim: String = "sub")
これにより、次のようにアプリケーションセット全体の柔軟性が向上します。
Java
Kotlin
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser("user_id") String userId) {
// .. find messages for this user and return them ...
}
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@CurrentUser("user_id") userId: String?): ModelAndView {
// .. find messages for this user and return them ...
}
Spring MVC 非同期統合
Spring Web MVC 3.2 + は、非同期リクエスト処理を優れた方法でサポートしています。追加の構成がない場合、Spring Security は、コントローラーから返された Callable
を呼び出す Thread
に SecurityContext
を自動的にセットアップします。例: 次のメソッドでは、Callable
の作成時に使用可能だった SecurityContext
を使用して Callable
が自動的に呼び出されます。
Java
Kotlin
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public Object call() throws Exception {
// ...
return "someView";
}
};
}
@RequestMapping(method = [RequestMethod.POST])
open fun processUpload(file: MultipartFile?): Callable<String> {
return Callable {
// ...
"someView"
}
}
SecurityContext と Callable の関連付け より技術的に言えば、Spring Security は |
コントローラーから返される DeferredResult
との自動統合はありません。これは、DeferredResult
がユーザーによって処理されるため、DeferredResult
と自動的に統合する方法がないためです。ただし、並行性サポートを使用して、Spring Security との透過的な統合を提供することはできます。
Spring MVC と CSRF の統合
Spring Security は Spring MVC と統合して、CSRF 保護を追加します。
自動トークンインクルージョン
Spring Security は、Spring MVC フォームタグを使用するフォーム内に CSRF トークンを自動的に含めます。次の JSP を検討してください。
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form" version="2.0">
<jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<!-- ... -->
<c:url var="logoutUrl" value="/logout"/>
<form:form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form:form>
<!-- ... -->
</html>
</jsp:root>
上記の例では、次のような HTML が出力されます。
<!-- ... -->
<form action="/context/logout" method="post">
<input type="submit" value="Log out"/>
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/>
</form>
<!-- ... -->
CsrfToken の解決
Spring Security は CsrfTokenArgumentResolver
を提供します。これは、Spring MVC 引数の現在の CsrfToken
を自動的に解決できます。@EnableWebSecurity を使用すると、これが Spring MVC 構成に自動的に追加されます。XML ベースの構成を使用する場合は、これを自分で追加する必要があります。
CsrfTokenArgumentResolver
が適切に構成されたら、CsrfToken
を静的 HTML ベースのアプリケーションに公開できます。
Java
Kotlin
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
@RestController
class CsrfController {
@RequestMapping("/csrf")
fun csrf(token: CsrfToken): CsrfToken {
return token
}
}
CsrfToken
を他のドメインから秘密にしておくことが重要です。つまり、クロスオリジン共有 (CORS) [Mozilla] を使用する場合は、CsrfToken
を外部ドメインに公開しないでください。