37. Spring MVC 集成

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

37.1 @EnableWebMvcSecurity

Note

从 Spring Security 4.0 开始,不推荐使用@EnableWebMvcSecurity。替换为@EnableWebSecurity,它将根据 Classpath 确定添加 Spring MVC 功能。

要启用与 Spring MVC 的 Spring Security 集成,请在配置中添加@EnableWebSecurityComments。

Note

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

37.2 MvcRequestMatcher

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

为了使用MvcRequestMatcher,您必须将 Spring Security Configuration 与DispatcherServlet放在相同的ApplicationContext中。这是必需的,因为 Spring Security 的MvcRequestMatcher期望名称为mvcHandlerMappingIntrospectorHandlerMappingIntrospector 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 s 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

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

通过在HttpServletRequest上进行匹配来提供授权规则是很好的,因为它发生在代码路径的早期,并且有助于减少attack surface。方法安全性可确保如果有人绕过了 Web 授权规则,则您的应用程序仍然受到保护。这就是所谓的深度防御

考虑一个 Map 如下的控制器:

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

如果我们想将对这种控制器方法的访问限制为 Management 员用户,则开发人员可以通过在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都将要求经过身份验证的用户为 Management 员用户。但是,根据我们的 Spring MVC 配置,URL /admin.html也将 Map 到我们的admin()方法。另外,根据我们的 Spring MVC 配置,URL /admin/也将 Map 到我们的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>

37.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 ObjectObject。可以使用以下代码访问当前已认证用户的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 开始,我们可以通过添加 Comments 来更直接地解析参数。例如:

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并提供名为getCustomUser的方法来访问CustomUserObject。例如,它可能看起来像:

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

然后,我们可以使用以Authentication.getPrincipal()作为根对象的SpEL expression来访问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 来 Management 用户,并且想要修改并保存当前用户的属性,则可以使用以下内容。

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

	// ...
}

我们可以通过在自己的 Comments 上设置@AuthenticationPrincipal作为元 Comments 来进一步消除对 Spring Security 的依赖。下面我们演示如何在名为@CurrentUser的 Comments 上执行此操作。

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

37.4 Spring MVC 异步集成

Spring Web MVC 3.2 对异步请求处理提供了出色的支持。如果没有其他配置,Spring Security 将自动将SecurityContext设置为Thread,该Thread执行您的控制器返回的Callable。例如,以下方法将自动使用创建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

从技术上讲,Spring Security 与WebAsyncManager集成。用于处理CallableSecurityContext是在调用startCallableProcessingSecurityContextHolder上存在的SecurityContext

没有与控制器返回的DeferredResult自动集成。这是因为DeferredResult由用户处理,因此无法自动与其集成。但是,您仍然可以使用Concurrency Support提供与 Spring Security 的透明集成。

37.5 Spring MVC 和 CSRF 集成

37.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>

<!-- ... -->

37.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成为其他域的 Secret。这意味着,如果您使用的是跨源共享(CORS),则应 不要CsrfToken公开给任何外部域。