Spring MVC 集成
Spring Security 提供了一些与 Spring MVC 的可选集成。本节将更详细地介绍这种集成。
@EnableWebMvcSecurity
从 Spring Security 4.0 开始, |
要启用 Spring Security 与 Spring MVC 的集成,请在您的配置中添加 @EnableWebSecurity
注解。
Spring Security 通过使用 Spring MVC 的 |
MvcRequestMatcher
Spring Security 与 Spring MVC 如何使用 MvcRequestMatcher
匹配 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 会自动创建一个MvcRequestMatcher
,如果它检测到 Spring MVC 在类路径中可用。因此,它将保护 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 Security 中解耦您的 Spring MVC 层。
考虑这样一种情况,自定义的UserDetailsService
返回一个实现UserDetails
的Object
和您自己的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 ...
}
有时,您可能需要以某种方式转换主体。例如,如果CustomUser
需要是最终的,则不能扩展它。在这种情况下,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
}
然后,我们可以使用SpEL 表达式来访问CustomUser
,该表达式使用Authentication.getPrincipal()
作为根对象
-
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 ...
}
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
,它可以自动解析 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
公开给任何外部域。