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,用于执行匹配。
对于 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
,它可以自动为 Spring MVC 参数解析当前 Authentication.getPrincipal()
。通过使用 @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
-
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
接口的对象,并提供一个名为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
的线程。例如,以下方法会自动调用其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
公开给任何外部域。