# Spring MVC集成 Spring 安全性提供了与 Spring MVC的许多可选集成。本节将进一步详细介绍集成。 ## @enableWebMVCSecurity | |在 Spring Security4.0中,`@EnableWebMvcSecurity`已被弃用。
替换为`@EnableWebSecurity`,这将决定在 Classpath 的基础上添加 Spring MVC特性。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 要启用 Spring 与 Spring MVC的安全集成,请在配置中添加`@EnableWebSecurity`注释。 | |Spring 安全性提供了使用 Spring MVC的的配置。这意味着,如果你正在使用更高级的选项,比如直接与集成,那么你将需要手动提供 Spring 安全性配置。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## MVCrequestMatcher Spring 安全性提供了与 Spring MVC如何在具有`MvcRequestMatcher`的URL上匹配的深度集成。这有助于确保你的安全规则与用于处理请求的逻辑相匹配。 为了使用`MvcRequestMatcher`,你必须将 Spring 安全配置放在与你的`DispatcherServlet`相同的`ApplicationContext`中。这是必要的,因为 Spring Security的`MvcRequestMatcher`期望名称为`HandlerMappingIntrospector` Bean 的`mvcHandlerMappingIntrospector`被用于执行匹配的 Spring MVC配置注册。 对于`web.xml`,这意味着你应该将配置放在`DispatcherServlet.xml`中。 ``` org.springframework.web.context.ContextLoaderListener contextConfigLocation /WEB-INF/spring/*.xml spring org.springframework.web.servlet.DispatcherServlet contextConfigLocation spring / ``` 下面`WebSecurityConfiguration`中置于`DispatcherServlet`s`ApplicationContext`中。 Java ``` 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[] { "/" }; } } ``` Kotlin ``` class SecurityInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { override fun getRootConfigClasses(): Array>? { return null } override fun getServletConfigClasses(): Array> { return arrayOf( RootConfiguration::class.java, WebMvcConfiguration::class.java ) } override fun getServletMappings(): Array { return arrayOf("/") } } ``` | |始终建议通过匹配`HttpServletRequest`和方法安全性来提供授权规则。

通过匹配`HttpServletRequest`来提供授权规则是很好的,因为它在代码路径中很早就发生了,并且有助于减少
方法安全性确保如果有人绕过了Web授权规则,那么你的应用程序仍然是安全的。
这就是所谓的[纵深防御](https://en.wikipedia.org/wiki/Defense_in_depth_(computing))| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 考虑一个映射如下的控制器: Java ``` @RequestMapping("/admin") public String admin() { ``` Kotlin ``` @RequestMapping("/admin") fun admin(): String { ``` 如果我们希望将对此控制器方法的访问限制为管理用户,那么开发人员可以通过在`HttpServletRequest`上匹配以下内容来提供授权规则: Java ``` protected configure(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .antMatchers("/admin").hasRole("ADMIN") ); } ``` Kotlin ``` override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN")) } } } ``` 或在XML中 ``` ``` 对于任一种配置,URL`/admin`将要求经过身份验证的用户是管理用户。然而,根据我们的 Spring MVC配置,URL`/admin.html`也将映射到我们的`admin()`方法。此外,根据我们的 Spring MVC配置,URL`/admin/`也将映射到我们的`admin()`方法。 问题在于,我们的安全规则仅保护`/admin`。我们可以为 Spring MVC的所有排列添加额外的规则,但这将是非常冗长和乏味的。 相反,我们可以利用 Spring security的`MvcRequestMatcher`。下面的配置将通过使用 Spring MVC在URL上进行匹配来保护与 Spring MVC匹配的相同的URL。 Java ``` protected configure(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/admin").hasRole("ADMIN") ); } ``` Kotlin ``` override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize("/admin", hasRole("ADMIN")) } } } ``` 或在XML中 ``` ``` ## @AuthenticationPrincipal Spring 安全性提供了`AuthenticationPrincipalArgumentResolver`,它可以自动解析 Spring MVC参数的当前`Authentication.getPrincipal()`。通过使用`@EnableWebSecurity`,你将自动将其添加到 Spring MVC配置中。如果使用基于XML的配置,则必须自己添加该配置。例如: ``` ``` 一旦`AuthenticationPrincipalArgumentResolver`被正确配置,你就可以在 Spring MVC层中与 Spring 安全性完全解耦。 考虑一种情况,其中一个自定义`UserDetailsService`返回一个`Object`,它实现`UserDetails`和你自己的`CustomUser``Object`。可以使用以下代码访问当前经过身份验证的用户的`CustomUser`: Java ``` @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 ... } ``` Kotlin ``` @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 Security3.2开始,我们可以通过添加一个注释来更直接地解决该参数。例如: Java ``` import org.springframework.security.core.annotation.AuthenticationPrincipal; // ... @RequestMapping("/messages/inbox") public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) { // .. find messages for this user and return them ... } ``` Kotlin ``` @RequestMapping("/messages/inbox") open fun findMessagesForUser(@AuthenticationPrincipal customUser: CustomUser?): ModelAndView { // .. find messages for this user and return them ... } ``` 有时可能有必要以某种方式转换本金。例如,如果`CustomUser`需要是最终的,则不能对其进行扩展。在这种情况下,`UserDetailsService`可能返回一个`Object`,它实现`UserDetails`并提供一个名为`getCustomUser`的方法来访问`CustomUser`。例如,它可能看起来像: Java ``` public class CustomUserUserDetails extends User { // ... public CustomUser getCustomUser() { return customUser; } } ``` Kotlin ``` class CustomUserUserDetails( username: String?, password: String?, authorities: MutableCollection? ) : User(username, password, authorities) { // ... val customUser: CustomUser? = null } ``` 然后,我们可以使用一个[Spel表达式](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html)访问`CustomUser`,它使用`Authentication.getPrincipal()`作为根对象: Java ``` 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 ... } ``` Kotlin ``` 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 ``` 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); // ... } ``` Kotlin ``` 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 安全性的依赖。下面我们将演示如何在名为`@CurrentUser`的注释上实现这一点。 | |重要的是要认识到,为了消除对 Spring 安全性的依赖关系,将创建`@CurrentUser`的是消耗应用程序。
这一步骤并不是严格必需的,但有助于将你对 Spring 安全性的依赖关系隔离到一个更中心的位置。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Java ``` @Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @AuthenticationPrincipal public @interface CurrentUser {} ``` Kotlin ``` @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @AuthenticationPrincipal annotation class CurrentUser ``` 现在已经指定了`@CurrentUser`,我们可以使用它来发送信号,以解析当前已验证用户的`CustomUser`。我们还将对 Spring 安全性的依赖隔离到一个文件中。 Java ``` @RequestMapping("/messages/inbox") public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) { // .. find messages for this user and return them ... } ``` Kotlin ``` @RequestMapping("/messages/inbox") open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView { // .. find messages for this user and return them ... } ``` ## Spring MVC异步集成 Spring Web MVC3.2+对[异步请求处理](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-async)具有出色的支持。 Spring 在没有额外配置的情况下,Security将自动将`SecurityContext`设置为调用控制器返回的`Thread`的`Callable`。例如,下面的方法将自动使用创建`Callable`时可用的`SecurityContext`调用其`Callable`: Java ``` @RequestMapping(method=RequestMethod.POST) public Callable processUpload(final MultipartFile file) { return new Callable() { public Object call() throws Exception { // ... return "someView"; } }; } ``` Kotlin ``` @RequestMapping(method = [RequestMethod.POST]) open fun processUpload(file: MultipartFile?): Callable { return Callable { // ... "someView" } } ``` | |将SecurityContext与Callable的

更严格地说, Spring Security与`WebAsyncManager`集成。
用于处理`Callable`的`SecurityContext`是在调用`SecurityContextHolder`时存在于`SecurityContextHolder`上的`SecurityContext`。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 没有控制器返回的`DeferredResult`的自动集成。这是因为`DeferredResult`是由用户处理的,因此无法自动与其集成。然而,你仍然可以使用[并发支持](../../features/integrations/concurrency.html#concurrency)来提供具有 Spring 安全性的透明集成。 ## Spring MVC和CSRF集成 ### 自动令牌包含 Spring 在使用[Spring MVC form tag](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/view.html#view-jsp-formtaglib-formtag)的窗体中,安全性将自动[包括CSRF令牌](../exploits/csrf.html#servlet-csrf-include)。例如,下面的JSP: ``` ``` 将输出类似于以下内容的HTML: ```
``` ### 解析CSRFToken Spring Security提供了`CsrfTokenArgumentResolver`,它可以自动解析 Spring MVC参数的当前`CsrfToken`。通过使用[@enableWebSecurity](../configuration/java.html#jc-hello-wsca),你将自动将其添加到 Spring MVC配置中。如果使用基于XML的配置,则必须自己添加该配置。 一旦正确配置了`CsrfTokenArgumentResolver`,就可以将`CsrfToken`公开到基于HTML的静态应用程序中。 Java ``` @RestController public class CsrfController { @RequestMapping("/csrf") public CsrfToken csrf(CsrfToken token) { return token; } } ``` Kotlin ``` @RestController class CsrfController { @RequestMapping("/csrf") fun csrf(token: CsrfToken): CsrfToken { return token } } ``` 保持`CsrfToken`是其他域的秘密是很重要的。这意味着如果你使用[跨源共享](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS),那么你应该**NOT**将`CsrfToken`公开到任何外部域。 [Spring Data](data.html)[WebSocket](websocket.html)