Spring MVC 整合
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
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
期望一个名称为 mvcHandlerMappingIntrospector
的 HandlerMappingIntrospector
Bean被你的Spring MVC配置注册,用于执行匹配。
对于 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("/")
}
}
我们总是建议你通过对 通过对 |
考虑一个Controller,它有如下mapping。
-
Java
-
Kotlin
@RequestMapping("/admin")
public String admin() {
// ...
}
@RequestMapping("/admin")
fun admin(): String {
// ...
}
为了限制管理员用户对这个Controller方法的访问,你可以通过在 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在classpath中可用,它会自动创建一个 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
的 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 ...
}
有时,你可能需要以某种方式转变 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
}
然后我们可以通过使用 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
已经被指定,我们可以用它来发出信号(signal)来解决我们的当前认证用户的 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
设置为调用 Controller 返回的 Callable
的 Thread
。例如,下面的方法会自动用创建 Callable
时可用的 SecurityContext
来调用它的 Callable
。
-
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"
}
}
Associating SecurityContext to Callable’s
将 |
没有与 Controller 返回的 DeferredResult
的自动整合。这是因为 DeferredResult
是由用户处理的,因此,没有办法与它自动整合。然而,你仍然可以使用 并发支持来提供与Spring Security的透明整合。
Spring MVC 和 CSRF 整合
Spring Security与Spring MVC集成,增加CSRF保护。
自动包含 Token
Spring Security 在使用 Spring MVC表单标签 的表单中自动 [包含CSRF Token。考虑一下下面的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
的 secret 是很重要的。这意味着,如果你使用 跨源资源共享(CORS),你不应该将 CsrfToken
暴露给任何外部域。