Spring MVC 集成
Spring Security 提供了许多与 Spring MVC 的可选集成。本节将详细介绍这些集成。
@EnableWebMvcSecurity
从 Spring Security 4.0 开始, |
要启用 Spring Security 与 Spring MVC 的集成,请将 @EnableWebSecurity
注解添加到您的配置中。
Spring Security 使用 Spring MVC 的 |
MvcRequestMatcher
Spring Security 通过 MvcRequestMatcher
与 Spring MVC 的 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
中。
-
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 path 属性,为此您可以使用 MvcRequestMatcher.Builder
创建多个共享相同 servlet path 的 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
对象的 Object
。可以通过以下代码访问当前认证用户的 CustomUser
:
-
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 ...
}
有时,您可能需要以某种方式转换 principal。例如,如果 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 的依赖,使用此注解的应用程序将创建 |
-
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 作为 principal 并且想要指定要检索哪个 claim 时。作为元注解,您可以这样做:
-
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
这已经相当强大了。但是,它也仅限于检索 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 集成到 |
与控制器返回的 DeferredResult
没有自动集成。这是因为 DeferredResult
由用户处理,因此无法自动与其集成。但是,您仍然可以使用并发支持来提供与 Spring Security 的透明集成。
Spring MVC 和 CSRF 集成
Spring Security 与 Spring MVC 集成以添加 CSRF 保护。
自动令牌包含
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
,它可以自动解析当前 CsrfToken
用于 Spring MVC 参数。通过使用@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
暴露给任何外部域。