# Spring MVC集成

Spring 安全性提供了与 Spring MVC的许多可选集成。本节将进一步详细介绍集成。

## @enableWebMVCSecurity

|   |在 Spring Security4.0中，`@EnableWebMvcSecurity`已被弃用。<br/>替换为`@EnableWebSecurity`，这将决定在 Classpath 的基础上添加 Spring MVC特性。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

要启用 Spring 与 Spring MVC的安全集成，请在配置中添加`@EnableWebSecurity`注释。

|   |Spring 安全性提供了使用 Spring MVC的的配置。这意味着，如果你正在使用更高级的选项，比如直接与集成，那么你将需要手动提供 Spring 安全性配置。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## MVCrequestMatcher

Spring 安全性提供了与 Spring MVC如何在具有`MvcRequestMatcher`的URL上匹配的深度集成。这有助于确保你的安全规则与用于处理请求的逻辑相匹配。

为了使用`MvcRequestMatcher`，你必须将 Spring 安全配置放在与你的`DispatcherServlet`相同的`ApplicationContext`中。这是必要的，因为 Spring Security的`MvcRequestMatcher`期望名称为`HandlerMappingIntrospector` Bean 的`mvcHandlerMappingIntrospector`被用于执行匹配的 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`s`ApplicationContext`中。

Java

```
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[] { "/" };
  }
}
```

Kotlin

```
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`和方法安全性来提供授权规则。<br/><br/>通过匹配`HttpServletRequest`来提供授权规则是很好的，因为它在代码路径中很早就发生了，并且有助于减少<br/>方法安全性确保如果有人绕过了Web授权规则，那么你的应用程序仍然是安全的。<br/>这就是所谓的[纵深防御](https://en.wikipedia.org/wiki/Defense_in_depth_(computing)）|
|---| |

考虑一个映射如下的控制器：

Java

```
@RequestMapping("/admin")
public String admin() {
```

Kotlin

```
@RequestMapping("/admin")
fun admin(): String {
```

如果我们希望将对此控制器方法的访问限制为管理用户，那么开发人员可以通过在`HttpServletRequest`上匹配以下内容来提供授权规则：

Java

```
protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests(authorize -> authorize
			.antMatchers("/admin").hasRole("ADMIN")
		);
}
```

Kotlin

```
override fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN"))
        }
    }
}
```

或在XML中

```
<http>
	<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
```

对于任一种配置，URL`/admin`将要求经过身份验证的用户是管理用户。然而，根据我们的 Spring MVC配置，URL`/admin.html`也将映射到我们的`admin()`方法。此外，根据我们的 Spring MVC配置，URL`/admin/`也将映射到我们的`admin()`方法。

问题在于，我们的安全规则仅保护`/admin`。我们可以为 Spring MVC的所有排列添加额外的规则，但这将是非常冗长和乏味的。

相反，我们可以利用 Spring security的`MvcRequestMatcher`。下面的配置将通过使用 Spring MVC在URL上进行匹配来保护与 Spring MVC匹配的相同的URL。

Java

```
protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests(authorize -> authorize
			.mvcMatchers("/admin").hasRole("ADMIN")
		);
}
```

Kotlin

```
override fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize("/admin", hasRole("ADMIN"))
        }
    }
}
```

或在XML中

```
<http request-matcher="mvc">
	<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
```

## @AuthenticationPrincipal

Spring 安全性提供了`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 安全性完全解耦。

考虑一种情况，其中一个自定义`UserDetailsService`返回一个`Object`，它实现`UserDetails`和你自己的`CustomUser``Object`。可以使用以下代码访问当前经过身份验证的用户的`CustomUser`:

Java

```
@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 ...
}
```

Kotlin

```
@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 Security3.2开始，我们可以通过添加一个注释来更直接地解决该参数。例如：

Java

```
import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

	// .. find messages for this user and return them ...
}
```

Kotlin

```
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@AuthenticationPrincipal customUser: CustomUser?): ModelAndView {

    // .. find messages for this user and return them ...
}
```

有时可能有必要以某种方式转换本金。例如，如果`CustomUser`需要是最终的，则不能对其进行扩展。在这种情况下，`UserDetailsService`可能返回一个`Object`，它实现`UserDetails`并提供一个名为`getCustomUser`的方法来访问`CustomUser`。例如，它可能看起来像：

Java

```
public class CustomUserUserDetails extends User {
		// ...
		public CustomUser getCustomUser() {
				return customUser;
		}
}
```

Kotlin

```
class CustomUserUserDetails(
    username: String?,
    password: String?,
    authorities: MutableCollection<out GrantedAuthority>?
) : User(username, password, authorities) {
    // ...
    val customUser: CustomUser? = null
}
```

然后，我们可以使用一个[Spel表达式](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html)访问`CustomUser`，它使用`Authentication.getPrincipal()`作为根对象：

Java

```
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 ...
}
```

Kotlin

```
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

```
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);

	// ...
}
```

Kotlin

```
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 安全性的依赖。下面我们将演示如何在名为`@CurrentUser`的注释上实现这一点。

|   |重要的是要认识到，为了消除对 Spring 安全性的依赖关系，将创建`@CurrentUser`的是消耗应用程序。<br/>这一步骤并不是严格必需的，但有助于将你对 Spring 安全性的依赖关系隔离到一个更中心的位置。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Java

```
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}
```

Kotlin

```
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal
annotation class CurrentUser
```

现在已经指定了`@CurrentUser`，我们可以使用它来发送信号，以解析当前已验证用户的`CustomUser`。我们还将对 Spring 安全性的依赖隔离到一个文件中。

Java

```
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {

	// .. find messages for this user and return them ...
}
```

Kotlin

```
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView {

    // .. find messages for this user and return them ...
}
```

## Spring MVC异步集成

Spring Web MVC3.2+对[异步请求处理](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-async)具有出色的支持。 Spring 在没有额外配置的情况下，Security将自动将`SecurityContext`设置为调用控制器返回的`Thread`的`Callable`。例如，下面的方法将自动使用创建`Callable`时可用的`SecurityContext`调用其`Callable`:

Java

```
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

return new Callable<String>() {
	public Object call() throws Exception {
	// ...
	return "someView";
	}
};
}
```

Kotlin

```
@RequestMapping(method = [RequestMethod.POST])
open fun processUpload(file: MultipartFile?): Callable<String> {
    return Callable {
        // ...
        "someView"
    }
}
```

|   |将SecurityContext与Callable的<br/><br/>更严格地说， Spring Security与`WebAsyncManager`集成。<br/>用于处理`Callable`的`SecurityContext`是在调用`SecurityContextHolder`时存在于`SecurityContextHolder`上的`SecurityContext`。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

没有控制器返回的`DeferredResult`的自动集成。这是因为`DeferredResult`是由用户处理的，因此无法自动与其集成。然而，你仍然可以使用[并发支持](../../features/integrations/concurrency.html#concurrency)来提供具有 Spring 安全性的透明集成。

## Spring MVC和CSRF集成

### 自动令牌包含

Spring 在使用[Spring MVC form tag](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/view.html#view-jsp-formtaglib-formtag)的窗体中，安全性将自动[包括CSRF令牌](../exploits/csrf.html#servlet-csrf-include)。例如，下面的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](../configuration/java.html#jc-hello-wsca)，你将自动将其添加到 Spring MVC配置中。如果使用基于XML的配置，则必须自己添加该配置。

一旦正确配置了`CsrfTokenArgumentResolver`，就可以将`CsrfToken`公开到基于HTML的静态应用程序中。

Java

```
@RestController
public class CsrfController {

	@RequestMapping("/csrf")
	public CsrfToken csrf(CsrfToken token) {
		return token;
	}
}
```

Kotlin

```
@RestController
class CsrfController {
    @RequestMapping("/csrf")
    fun csrf(token: CsrfToken): CsrfToken {
        return token
    }
}
```

保持`CsrfToken`是其他域的秘密是很重要的。这意味着如果你使用[跨源共享](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)，那么你应该**NOT**将`CsrfToken`公开到任何外部域。

[Spring Data](data.html)[WebSocket](websocket.html)

