对于最新的稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

Spring MVC 集成

Spring Security 提供了多个与 Spring MVC 的可选集成。 本节将更详细地介绍这些集成。spring-doc.cadn.net.cn

启用 Web MVC 安全

自 Spring Security 4.0 起,@EnableWebMvcSecurity 已弃用。 替换为 @EnableWebSecurity,它基于类路径添加了 Spring MVC 功能。spring-doc.cadn.net.cn

要启用 Spring Security 与 Spring MVC 的集成,请在您的配置中添加 @EnableWebSecurity 注解。spring-doc.cadn.net.cn

Spring Security 使用 Spring MVC 的 WebMvcConfigurer 提供配置。 这意味着,如果您使用更高级的选项,例如直接与 WebMvcConfigurationSupport 集成,则需要手动提供 Spring Security 配置。spring-doc.cadn.net.cn

路径模式请求匹配器

Spring Security 通过 PathPatternRequestMatcher 与 Spring MVC 的 URL 匹配机制实现了深度集成。 这有助于确保您的安全规则与处理请求时所使用的逻辑保持一致。spring-doc.cadn.net.cn

PathPatternRequestMatcher 必须使用与 Spring MVC 相同的 PathPatternParser。 如果你没有自定义 PathPatternParser,那么你可以这样做:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

如果您正在自定义 Spring MVC 的 PathPatternParser 实例,您需要在同一个 ApplicationContext 中配置 Spring Security 和 Spring MVCspring-doc.cadn.net.cn

我们始终建议您通过匹配 HttpServletRequest 和方法安全来提供授权规则。spring-doc.cadn.net.cn

通过匹配 HttpServletRequest 来提供授权规则是很好的做法,因为它发生在代码执行路径的非常早期阶段,有助于减少攻击面。 方法级安全确保即使有人绕过了 Web 授权规则,您的应用程序仍然受到保护。 这被称为纵深防御spring-doc.cadn.net.cn

现在 Spring MVC 已与 Spring Security 集成,您可以开始编写一些授权规则,这些规则将使用 PathPatternRequestMatcherspring-doc.cadn.net.cn

@AuthenticationPrincipal

Spring Security 提供了 AuthenticationPrincipalArgumentResolver,它可以自动为 Spring MVC 的方法参数解析当前的 Authentication.getPrincipal()。 通过使用 @EnableWebSecurity,该解析器会自动添加到你的 Spring MVC 配置中。 如果你使用基于 XML 的配置,则必须手动添加它:spring-doc.cadn.net.cn

<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。spring-doc.cadn.net.cn

考虑这样一种情况:一个自定义的 UserDetailsService 返回一个实现了 Object 接口的对象,同时该对象也是你自己的 UserDetails 类型。当前已认证用户的 CustomUser 对象可以通过以下代码进行访问:spring-doc.cadn.net.cn

@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 开始,我们可以通过添加一个注解更直接地解析该参数:spring-doc.cadn.net.cn

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

有时,你可能需要以某种方式转换主体(principal)。 例如,如果 CustomUser 需要是 final 的,则无法对其进行扩展。 在这种情况下,UserDetailsService 可能会返回一个实现了 Object 接口的 UserDetails,并提供一个名为 getCustomUser 的方法来访问 CustomUserspring-doc.cadn.net.cn

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
}

然后,我们可以通过使用一个SpEL 表达式来访问https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html,该表达式以Authentication.getPrincipal()作为根对象:spring-doc.cadn.net.cn

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 来管理用户,并且想要修改并保存当前用户的某个属性,可以使用以下方式:spring-doc.cadn.net.cn

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 作为元注解(meta-annotation)应用到我们自己的注解上,进一步消除对 Spring Security 的依赖。 下面的示例演示了如何在名为 @CurrentUser 的注解上实现这一点。spring-doc.cadn.net.cn

为了移除对 Spring Security 的依赖,应由使用该功能的应用程序来创建 @CurrentUser。 此步骤并非严格必需,但有助于将您对 Spring Security 的依赖集中到一个更中心化的位置。spring-doc.cadn.net.cn

@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 已经指定,我们可以使用它来指示解析当前已认证用户的 CustomUserspring-doc.cadn.net.cn

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

一旦它成为元注解,参数化功能也可供您使用。spring-doc.cadn.net.cn

例如,假设你使用 JWT 作为你的主体(principal),并希望指定要检索哪个声明(claim)。 作为一种元注解(meta-annotation),你可以这样做:spring-doc.cadn.net.cn

@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 声明。spring-doc.cadn.net.cn

为了提高灵活性,首先像下面这样发布 AnnotationTemplateExpressionDefaults bean:spring-doc.cadn.net.cn

@Bean
public AnnotationTemplateExpressionDefaults templateDefaults() {
	return new AnnotationTemplateExpressionDefaults();
}
@Bean
fun templateDefaults(): AnnotationTemplateExpressionDefaults {
	return AnnotationTemplateExpressionDefaults()
}
<b:bean name="annotationExpressionTemplateDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/>

然后你可以像这样为 @CurrentUser 提供一个参数:spring-doc.cadn.net.cn

@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")

这将以以下方式为您的应用程序集合提供更大的灵活性:spring-doc.cadn.net.cn

@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 Security 提供了 CurrentSecurityContextArgumentResolver,它可以自动为 Spring MVC 的方法参数解析当前的 SecurityContext。 通过使用 @EnableWebSecurity,该解析器会自动添加到你的 Spring MVC 配置中。 如果你使用基于 XML 的配置,则必须手动添加它:spring-doc.cadn.net.cn

<mvc:annotation-driven>
		<mvc:argument-resolvers>
				<bean class="org.springframework.security.web.method.annotation.CurrentSecurityContextArgumentResolver" />
		</mvc:argument-resolvers>
</mvc:annotation-driven>

一旦配置了 CurrentSecurityContextArgumentResolver,你就可以直接访问 SecurityContextspring-doc.cadn.net.cn

@GetMapping("/me")
public String me(@CurrentSecurityContext SecurityContext context) {
	return context.getAuthentication().getName();
}
@GetMapping("/me")
fun me(@CurrentSecurityContext context: SecurityContext): String {
    return context.authentication.name
}

你也可以使用一个以 SecurityContext 为根的 SpEL 表达式:spring-doc.cadn.net.cn

@GetMapping("/me")
public String me(@CurrentSecurityContext(expression = "authentication") Authentication authentication) {
	return authentication.getName();
}
@GetMapping("/me")
fun me(@CurrentSecurityContext(expression = "authentication") authentication: Authentication): String {
    return authentication.name
}

Spring MVC 异步集成

Spring Web MVC 3.2+ 对异步请求处理提供了出色的支持。 无需额外配置,Spring Security 会自动将SecurityContext设置到调用控制器所返回的ThreadCallable上。 例如,以下方法中的Callable在被调用时,会自动使用创建该SecurityContext时可用的Callablespring-doc.cadn.net.cn

@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 进行集成。 用于处理 SecurityContextCallable,是在调用 SecurityContext 时存在于 SecurityContextHolder 中的 startCallableProcessingspring-doc.cadn.net.cn

控制器返回的 DeferredResult 没有自动集成。 这是因为 DeferredResult 由用户自行处理,因此无法对其进行自动集成。 不过,您仍然可以使用并发支持来实现与 Spring Security 的透明集成。spring-doc.cadn.net.cn

Spring MVC 与 CSRF 集成

Spring Security 与 Spring MVC 集成以添加 CSRF 保护。spring-doc.cadn.net.cn

自动Tokens包含

Spring Security 会自动在使用 Spring MVC 表单标签 的表单中包含 CSRF Tokens。 请考虑以下 JSP:spring-doc.cadn.net.cn

<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 类似于以下内容:spring-doc.cadn.net.cn

<!-- ... -->

<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 的配置,则必须手动添加此项。spring-doc.cadn.net.cn

一旦正确配置了 CsrfTokenArgumentResolver,您就可以将 CsrfToken 暴露给基于静态 HTML 的应用程序:spring-doc.cadn.net.cn

@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 暴露给任何外部域。spring-doc.cadn.net.cn

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

如果你使用的是 Spring Boot,默认情况下,Spring MVC 和 Spring Security 位于同一个应用上下文中。spring-doc.cadn.net.cn

否则,对于 Java 配置,同时包含 @EnableWebMvc@EnableWebSecurity 将在同一个上下文中构建 Spring Security 和 Spring MVC 组件。spring-doc.cadn.net.cn

或者,如果你正在使用 ServletListener,你可以这样做:spring-doc.cadn.net.cn

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 文件,你可以按如下方式配置 DispatcherServletspring-doc.cadn.net.cn

<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 被放置在 ApplicationContextDispatcherServlet 中。spring-doc.cadn.net.cn