Spring MVC 集成
Spring Security 提供了许多可选的与 Spring MVC 的集成。本节将更详细地介绍这些集成。
@EnableWebMvcSecurity
从 Spring Security 4.0 开始,@EnableWebMvcSecurity
已被弃用。替代方案是 @EnableWebSecurity
,它会根据类路径添加 Spring MVC 功能。
要启用 Spring Security 与 Spring MVC 的集成,在配置中添加 @EnableWebSecurity
注解。
Spring Security 通过使用 Spring MVC 的 WebMvcConfigurer 提供配置。这意味着,如果您使用更高级的选项,例如直接与 WebMvcConfigurationSupport
集成,则需要手动提供 Spring Security 配置。
MvcRequestMatcher
Spring Security 通过 MvcRequestMatcher
与 Spring MVC 的 URL 匹配方式提供了深度集成。这有助于确保你的安全规则与处理请求所用的逻辑相匹配。
要使用 MvcRequestMatcher
,您必须将 Spring Security 配置与 DispatcherServlet
放在同一个 ApplicationContext
中。这是必需的,因为 Spring Security 的 MvcRequestMatcher
期望您的 Spring MVC 配置注册一个名为 mvcHandlerMappingIntrospector
的 HandlerMappingIntrospector
bean,该 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
中。
- Java
- Kotlin
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("/")
}
}
考虑如下映射的控制器:
- Java
- Kotlin
@RequestMapping("/admin")
public String admin() {
// ...
}
@RequestMapping("/admin")
fun admin(): String {
// ...
}
要限制对此控制器方法的访问仅限管理员用户,可以通过匹配 HttpServletRequest
并使用以下规则来提供授权规则:
- Java
- Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin").hasRole("ADMIN")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/admin", hasRole("ADMIN"))
}
}
return http.build()
}
以下 XML 代码实现了相同的功能:
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
无论是哪种配置,/admin
URL 都要求经过身份验证的用户是管理员用户。然而,根据我们的 Spring MVC 配置,/admin.html
URL 也映射到我们的 admin()
方法。此外,根据我们的 Spring MVC 配置,/admin
URL 也映射到我们的 admin()
方法。
问题是我们的安全规则仅保护了 /admin
。我们可以为 Spring MVC 的所有排列组合添加额外的规则,但这会非常冗长和繁琐。
幸运的是,当使用 requestMatchers
DSL 方法时,如果 Spring Security 检测到类路径中存在 Spring MVC,它会自动创建一个 MvcRequestMatcher
。因此,它将通过使用 Spring MVC 匹配 URL 来保护 Spring MVC 将匹配的相同 URL。
在使用 Spring MVC 时,一个常见的要求是指定 servlet 路径属性,为此你可以使用 MvcRequestMatcher.Builder
来创建多个共享相同 servlet 路径的 MvcRequestMatcher
实例:
- Java
- Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val mvcMatcherBuilder = MvcRequestMatcher.Builder(introspector)
http {
authorizeHttpRequests {
authorize(mvcMatcherBuilder.pattern("/admin"), hasRole("ADMIN"))
authorize(mvcMatcherBuilder.pattern("/user"), hasRole("USER"))
}
}
return http.build()
}
以下 XML 具有相同的效果:
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
@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
接口和你自己的 CustomUser
对象的对象。可以通过以下代码访问当前已认证用户的 CustomUser
:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof CustomUser) {
CustomUser customUser = (CustomUser) principal;
// 进行其他操作
}
- Java
- Kotlin
@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 开始,我们可以通过添加一个注解更直接地解析参数:
- Java
- Kotlin
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 ...
}
有时,你可能需要以某种方式转换主体。例如,如果 CustomUser
需要是最终的(final),则不能对其进行扩展。在这种情况下,UserDetailsService
可能会返回一个实现 UserDetails
的 Object
,并提供一个名为 getCustomUser
的方法来访问 CustomUser
:
- Java
- Kotlin
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
}
然后,我们可以使用以 Authentication.getPrincipal()
作为根对象的SpEL 表达式来访问 CustomUser
:
- Java
- Kotlin
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来管理用户,并且想要修改和保存当前用户的某个属性,我们可以使用以下代码:
- Java
- Kotlin
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
作为我们自己的注解的元注解来进一步减少对 Spring Security 的依赖。下面的例子展示了我们如何在一个名为 @CurrentUser
的注解上实现这一点。
要移除对 Spring Security 的依赖,是由消费应用程序来创建 @CurrentUser
。这一步不是严格必需的,但有助于将你对 Spring Security 的依赖隔离到一个更中心的位置。
- Java
- Kotlin
@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
已经指定,我们可以使用它来解析当前已认证用户的 CustomUser
:
- Java
- Kotlin
@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 ...
}
一旦它成为元注解,参数化对你来说也是可用的。
例如,假设你的主体是一个 JWT,并且你想指定要检索的声明。作为元注解,你可以这样做:
- Java
- Kotlin
@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
which is already quite powerful. But, it is also limited to retrieving the sub
claim.
要使这更加灵活,首先像这样发布 AnnotationTemplateExpressionDefaults
bean:
- Java
- Kotlin
- Xml
@Bean
public AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDeafults();
}
@Bean
fun templateDefaults(): AnnotationTemplateExpressionDefaults {
return AnnotationTemplateExpressionDeafults()
}
<b:bean name="annotationExpressionTemplateDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/>
然后你可以像这样为 @CurrentUser
提供一个参数:
- Java
- Kotlin
@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")
这将以以下方式为你的应用程序集提供更多的灵活性:
- Java
- Kotlin
@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 MVC 异步集成
Spring Web MVC 3.2+ 对 异步请求处理 有很好的支持。无需额外配置,Spring Security 会自动将 SecurityContext
设置到调用控制器返回的 Callable
的 Thread
中。例如,以下方法的 Callable
会在创建 Callable
时可用的 SecurityContext
下被自动调用:
- Java
- Kotlin
@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
集成。用于处理 Callable
的 SecurityContext
是在调用 startCallableProcessing
时存在于 SecurityContextHolder
上的 SecurityContext
。
没有与控制器返回的 DeferredResult
的自动集成。这是因为 DeferredResult
是由用户处理的,因此无法自动集成。但是,您仍然可以使用 并发支持 来提供与 Spring Security 的透明集成。
Spring MVC 和 CSRF 集成
Spring Security 与 Spring MVC 集成以添加 CSRF 保护。
自动 Token 包含
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>
<!-- ... -->
解决 CsrfToken
Spring Security 提供了 CsrfTokenArgumentResolver
,它可以自动解析 Spring MVC 参数中的当前 CsrfToken
。通过使用 @EnableWebSecurity,您可以自动将其添加到 Spring MVC 配置中。如果您使用基于 XML 的配置,则必须自己添加。
一旦正确配置了 CsrfTokenArgumentResolver
,你就可以将 CsrfToken
暴露给基于静态 HTML 的应用程序:
- Java
- Kotlin
@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
暴露给任何外部域。