跳到主要内容
版本:7.0.2

Spring MVC 集成

DeepSeek V3 中英对照 Spring MVC Spring MVC Integration

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,则可以执行以下操作:

@Bean
PathPatternRequestMatcherBuilderFactoryBean usePathPattern() {
return new 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,它可以自动解析当前 Authentication.getPrincipal() 作为 Spring MVC 方法的参数。通过使用 @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 接口的 Object,并且这个 Object 是你自己的 CustomUser 对象。当前已认证用户的 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 需要被声明为 final,那么它将不能被继承。在这种情况下,UserDetailsService 可能会返回一个实现了 UserDetails 接口的对象,并提供一个名为 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 messages 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 的注解上实现这一点。

备注

为了移除对 Spring Security 的依赖,将由消费应用程序来创建 @CurrentUser。此步骤并非严格必需,但有助于将您对 Spring Security 的依赖隔离到更中心的位置。

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

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

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

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

一旦它成为元注解,参数化功能也同样可供你使用。

例如,假设您有一个 JWT 作为主体,并且您想要指定要检索哪个声明。作为元注解,您可以这样做:

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['sub']")
public @interface CurrentUser {}

这已经相当强大了。但,它也仅限于检索 sub 声明。

为了使其更加灵活,首先像这样发布 AnnotationTemplateExpressionDefaults bean:

@Bean
public AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDefaults();
}

然后你可以像这样给 @CurrentUser 提供一个参数:

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['{claim}']")
public @interface CurrentUser {
String claim() default 'sub';
}

这将通过以下方式为您的应用程序集提供更大的灵活性:

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser("user_id") String userId) {

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

Spring MVC 异步集成

Spring Web MVC 3.2+ 对异步请求处理提供了出色的支持。在无需额外配置的情况下,Spring Security 会自动将 SecurityContext 设置到调用控制器返回的 CallableThread 中。例如,以下方法会自动在创建 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";
}
};
}
备注

将 SecurityContext 关联到 Callable

从技术角度更精确地说,Spring Security 与 WebAsyncManager 集成。用于处理 CallableSecurityContext 是在调用 startCallableProcessing 时存在于 SecurityContextHolder 上的那个 SecurityContext

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

Spring MVC 与 CSRF 集成

Spring Security 与 Spring MVC 集成以提供 CSRF 防护。

自动令牌包含

Spring Security 会自动在表单中包含 CSRF 令牌,这些表单使用 Spring MVC 表单标签。考虑以下 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 的应用程序:

@RestController
public class CsrfController {

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

确保CsrfToken对其他域保密至关重要。这意味着,如果您使用跨源资源共享(CORS),则不应CsrfToken暴露给任何外部域。

在同一应用上下文中配置 Spring MVC 和 Spring Security

如果你正在使用Boot,默认情况下Spring MVC和Spring Security会位于同一个应用上下文中。

否则,对于Java配置,同时包含@EnableWebMvc@EnableWebSecurity注解将在同一上下文中构建Spring Security和Spring MVC组件。

或者,如果你正在使用 ServletListener,你可以这样做:

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

最后,对于一个 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 中。