最新の安定バージョンについては、Spring Security 6.3.1 を使用してください!

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

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("/")
    }
}

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

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

次のようにマップされているコントローラーを考えます。

  • 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 と独自の CustomUserObject を実装する 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 ...
}

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 を外部ドメインに公開しないでください。