39. Spring MVC 集成

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

39.1 @EnableWebMvcSecurity

[Note] Note

至于 Spring Security 4.0,不推荐 @EnableWebMvcSecurity。替换是 @EnableWebSecurity,它将决定基于类路径添加 Spring MVC 特性。

为了使 Spring Security 与 Spring MVC 集成,在你的配置中添加 @EnableWebSecurity 注解。

[Note] Note

Spring Security 使用 Spring MVC 的 WebMvcConfigurer 提供配置。 这意味着如果你正在使用更高级的选项,比如直接与 WebMvcConfigurationSupport 集成,那么你将需要手动提供 Spring Security 配置。

39.2 MvcRequestMatcher

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

为了使用 MvcRequestMatcher,必须将 Spring Security 配置放置在与 DispatcherServlet 相同的 ApplicationContext 中。这是必要的,因为 Spring Security 的 MvcRequestMatcher 希望通过用于执行匹配的 Spring MVC 配置注册名为 mvcHandlerMappingIntrospector 的 HandlerMappingIntrospector bean。

对于 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 中。

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

建议通过在 HttpServletRequest 和方法安全性上匹配来提供授权规则。

通过在 HttpServletRequest 上进行匹配来提供授权规则是很好的,因为它在代码路径中发生得非常早,并且有助于减少攻击面。方法安全性确保如果有人绕过 web 授权规则,你的应用程序仍然安全。这就是所谓的 深度防御

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

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

如果我们想限制对这个控制器方法的访问以管理用户,开发人员可以通过以下方式在 HttpServletRequest 上进行匹配来提供授权规则:

protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.antMatchers("/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。

protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.mvcMatchers("/admin").hasRole("ADMIN");
}

或者用 XML

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

39.3 @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:

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

在 Spring Security 3.2 中,我们可以通过添加注解来更直接地解决参数。例如:

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

// ...

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

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

有时可能有必要在某种程度上改变主体。例如,如果 CustomUser 需要是最后的,它不能被扩展。在这种情况下,UserDetailsService 可能返回一个实现 UserDetails 的 Object,并提供一个名为 getCustomUser 的方法来访问 CustomUser。例如,它可能看起来像:

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

然后,我们可以使用一个使用 Authentication.getPrincipal() 的 SpEL 表达式 来访问 CustomUser 作为根对象:

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

// ...

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

	// .. find messags for this user and return them ...
}

我们也可以在我们的 SpEL 表达式中引用 bean。例如,如果我们使用 JPA 来管理用户,并且希望修改和保存当前用户的属性,则可以使用以下内容。

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

	// ...
}

通过将 @AuthenticationPrincipal 作为我们自己的注解上的元注解,我们可以进一步消除对 Spring Security 的依赖。下面我们演示如何在一个名为 @CurrentUser 的注解上实现这一点。

[Note] Note

重要的是要认识到,为了消除对 Spring Security 的依赖,需要创建的 @CurrentUser 是消费应用程序。这一步骤不是严格要求的,但有助于隔离你对 Spring Security 的依赖性到一个更中心的位置。

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

既然已经指定了 @CurrentUser,我们就可以用它来解决当前认证用户的 CustomUser 问题。我们还将对 Spring Security 的依赖性隔离到一个文件中。

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

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

39.4 Spring MVC 异步集成

Spring Web MVC 3.2+ 对异步请求处理有极好的支持。如果没有其他配置,Spring Security 将自动将 SecurityContext 设置为执行控制器返回的 Callable 的 Thread。例如,以下方法将自动使用创建 Callable 时可用的 SecurityContext 执行其 Callable:

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

return new Callable<String>() {
	public Object call() throws Exception {
	// ...
	return "someView";
	}
};
}
[Note] Associating SecurityContext to Callable’s

从技术上讲,Spring Security 与 WebAsyncManager 集成在一起。用于处理 Callable 的 SecurityContext 是调用 startCallableProcessing 时在 SecurityContextHolder 上存在的 SecurityContext。

这没有一个自动集成,而是由控制器返回的 DeferredResult。这是因为 DeferredResult 是由用户处理的,因此无法与之自动集成。但是,仍然可以使用并发支持来提供与 Spring Security 的透明集成。

39.5 Spring MVC 和 CSRF 集成

39.5.1 自动令牌包含

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>

<!-- ... -->

39.5.2 解析 CsrfToken

Spring Security 提供了 CsrfTokenArgumentResolver,它可以自动解析 Spring MVC 参数的当前 CsrfToken。通过使用 @EnableWebSecurity,你将自动将此添加到 Spring MVC 配置中。如果使用基于 XML 的配置,则必须自己添加。

一旦 CsrfTokenArgumentResolver 被正确配置,你就可以将 CsrfToken 公开到基于静态 HTML 的应用程序。

@RestController
public class CsrfController {

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

重要的是要保持 CsrfToken 在其他域的秘密。这意味着,如果使用Cross Origin Sharing (CORS),则不应将 CsrfToken 公开给任何外部域。