Spring MVC 統合
Spring Security は、Spring MVC とのオプションの統合を多数提供します。このセクションでは、統合についてさらに詳しく説明します。
@EnableWebMvcSecurity
Spring Security 4.0 の時点で、 |
Spring Security と Spring MVC の統合を有効にするには、構成に @EnableWebSecurity
アノテーションを追加します。
Spring Security は、Spring MVC の |
PathPatternRequestMatcher
Spring Security は、Spring MVC が URL で PathPatternRequestMatcher
と一致する方法との緊密な統合を提供します。これは、セキュリティルールがリクエストの処理に使用されるロジックと一致することを確認できます。
PathPatternRequestMatcher
は Spring MVC と同じ PathPatternParser
を使用する必要があります。PathPatternParser
をカスタマイズしていない場合は、次のようにします。
Java
Kotlin
XML
@Bean
PathPatternRequestMatcherBuilderFactoryBean usePathPattern() {
return new PathPatternRequestMatcherBuilderFactoryBean();
}
@Bean
fun usePathPattern(): PathPatternRequestMatcherBuilderFactoryBean {
return PathPatternRequestMatcherBuilderFactoryBean()
}
<b:bean class="org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean"/>
Spring Security は適切な Spring MVC 構成を見つけます。
Spring MVC の PathPatternParser
インスタンスをカスタマイズする 場合は、同じ ApplicationContext
で Spring Security と Spring MVC を構成する必要があります。
|
Spring MVC が Spring Security と統合されたため、PathPatternRequestMatcher
を使用する認可ルールを記述する準備が整いました。
@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
を外部ドメインに公開しないでください。
同じアプリケーションコンテキストでの Spring MVC と Spring Security の構成
Boot を使用している場合、Spring MVC と Spring Security はデフォルトで同じアプリケーションコンテキストにあります。
それ以外の場合、Java 構成 では、@EnableWebMvc
と @EnableWebSecurity
の両方を含めると、同じコンテキストに Spring Security コンポーネントと Spring MVC コンポーネントが構築されます。
もちろん、ServletListener
を使用している場合は、次の操作を実行できます。
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("/")
}
}
最後に、web.xml
ファイルの場合は、次のように DispatcherServlet
を構成します。
<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
に配置されます。