Spring MVC 統合

Spring Security は、Spring MVC とのオプションの統合を多数提供します。このセクションでは、統合についてさらに詳しく説明します。

@EnableWebMvcSecurity

Spring Security 4.0 の時点で、@EnableWebMvcSecurity は非推奨になりました。代替は @EnableWebSecurity で、クラスパスに基づいて Spring MVC 機能を追加します。

Spring Security と Spring MVC の統合を有効にするには、構成に @EnableWebSecurity アノテーションを追加します。

Spring Security は、Spring MVC の WebMvcConfigurer を使用して構成を提供します。つまり、WebMvcConfigurationSupport と直接統合するなど、より高度なオプションを使用する場合は、Spring Security 構成を手動で提供する必要があります。

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 を構成する必要があります。

HttpServletRequest とメソッドのセキュリティを照合して、認可ルールを提供することを常にお勧めします。

HttpServletRequest で照合することによって認可ルールを提供することは、コードパスの非常に早い段階で発生し、攻撃対象領域 [Wikipedia] を減らすのに役立つため、優れています。メソッドセキュリティは、誰かが Web 認可ルールをバイパスした場合でも、アプリケーションが引き続き保護されることを保証します。これは多層防御 [Wikipedia] として知られています

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 への依存を取り除くために、@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 は WebAsyncManager と統合されます。Callable の処理に使用される SecurityContext は、startCallableProcessing が呼び出されたときに SecurityContextHolder に存在する SecurityContext です。

コントローラーから返される 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 に配置されます。