Spring MVC 整合

Spring Security 提供了多項與 Spring MVC 的可選整合。本節將詳細介紹這些整合。

@EnableWebSecurity

要啟用 Spring Security 與 Spring MVC 的整合,請在配置中新增 @EnableWebSecurity 註解。

Spring Security 透過使用 Spring MVC 的 WebMvcConfigurer 提供配置。這意味著,如果您使用更高階的選項,例如直接與 WebMvcConfigurationSupport 整合,則需要手動提供 Spring Security 配置。

PathPatternRequestMatcher

Spring Security 透過 PathPatternRequestMatcher 與 Spring MVC 如何匹配 URL 進行深度整合。這有助於確保您的安全規則與處理請求的邏輯相匹配。

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 來提供授權規則是好的,因為它在程式碼路徑中發生得很早,有助於減少攻擊面。方法安全性確保即使有人繞過了 Web 授權規則,您的應用程式仍然是安全的。這被稱為縱深防禦

現在 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 ObjectObject。可以使用以下程式碼訪問當前已認證使用者的 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 需要是 final,它就不能被擴充套件。在這種情況下,UserDetailsService 可能會返回一個實現 UserDetails 並提供一個名為 getCustomUser 的方法來訪問 CustomUserObject

  • 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 會自動將 SecurityContext 設定到呼叫控制器返回的 CallableThread 中。例如,以下方法的 Callable 會自動在建立 Callable 時可用的 SecurityContext 中被呼叫

  • 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 整合。用於處理 CallableSecurityContext 是在呼叫 startCallableProcessing 時存在於 SecurityContextHolder 中的 SecurityContext

對於控制器返回的 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),則不應CsrfToken 暴露給任何外部域。

在同一個 Application Context 中配置 Spring MVC 和 Spring Security

如果您使用 Boot,Spring MVC 和 Spring Security 預設在同一個應用程式上下文中。

否則,對於 Java Config,同時包含 @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 放置在 DispatcherServletApplicationContext 中。

© . This site is unofficial and not affiliated with VMware.