Spring Security提供了许多与Spring MVC的可选集成。本节将更详细地介绍集成。
37.1 @EnableWebMvcSecurity
从Spring Security 4.0开始,不推荐使用@ EnableWebMvcSecurity。替换为@EnableWebSecurity,它将根据类路径来决定是否添加Spring MVC特性。
要启用与Spring MVC的Spring安全集成,请在您的配置中添加@EnableWebSecurity注释。
Spring Security使用Spring MVC的WebMvcConfigurerAdapter提供配置。这意味着,如果您使用更高级的选项,如直接与WebMvcConfigurationSupport集成,那么您将需要手动提供Spring安全性配置。
37.2 MvcRequestMatcher
Spring Security提供了与Spring MVC如何在URL上与MvcRequestMatcher匹配的深度集成。这有助于确保您的安全规则与用于处理您的请求的逻辑相匹配。
为了使用MvcRequestMatcher,您必须将Spring安全配置放在与您的DispatcherServlet相同的应用程序上下文中ApplicationContext
。这是必要的,因为Spring Security的MvcRequestMatcher期望一个名为mvcHandlerMappingIntrospector的HandlerMappingIntrosspector bean用于执行匹配的Spring MVC配置来注册。
对于web.xml,这意味着您应该将您的配置放在DispatcherServlet.xml中。
1 <listener> 2 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 3 </listener> 4 5 <!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ --> 6 <context-param> 7 <param-name>contextConfigLocation</param-name> 8 <param-value>/WEB-INF/spring/*.xml</param-value> 9 </context-param> 10 11 <servlet> 12 <servlet-name>spring</servlet-name> 13 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 14 <!-- Load from the ContextLoaderListener --> 15 <init-param> 16 <param-name>contextConfigLocation</param-name> 17 <param-value></param-value> 18 </init-param> 19 </servlet> 20 21 <servlet-mapping> 22 <servlet-name>spring</servlet-name> 23 <url-pattern>/</url-pattern> 24 </servlet-mapping>
在WebSecurityConfiguration 下面,放在DispatcherServlets应用程序上下文中ApplicationContext。
1 public class SecurityInitializer extends 2 AbstractAnnotationConfigDispatcherServletInitializer { 3 4 @Override 5 protected Class<?>[] getRootConfigClasses() { 6 return null; 7 } 8 9 @Override 10 protected Class<?>[] getServletConfigClasses() { 11 return new Class[] { RootConfiguration.class,WebMvcConfiguration.class }; 13 } 14 15 @Override 16 protected String[] getServletMappings() { 17 return new String[] { "/" }; 18 } 19 }
始终建议通过匹配HttpServletRequest和方法安全性来提供授权规则。 通过在HttpServletRequest上匹配来提供授权规则是很好的,因为它发生在代码路径的早期,有助于减少攻击面。方法安全性确保如果有人绕过了web授权规则,您的应用程序仍然是安全的。这就是所谓的纵深防御。
考虑一个映射如下的控制器:
1 @RequestMapping("/admin") 2 public String admin() {
如果我们希望将对该控制器方法的访问限制在管理员用户,开发人员可以通过在HttpServletRequest上匹配以下内容来提供授权规则:
1 protected configure(HttpSecurity http) throws Exception { 2 http 3 .authorizeRequests() 4 .antMatchers("/admin").hasRole("ADMIN"); 5 }
或在xml
1 <http> 2 <intercept-url pattern="/admin" access="hasRole('ADMIN')"/> 3 </http>
无论采用哪种配置,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。
protected configure(HttpSecurity http) throws Exception { http .authorizeRequests() .mvcMatchers("/admin").hasRole("ADMIN"); }
或xml
1 <http request-matcher="mvc"> 2 <intercept-url pattern="/admin" access="hasRole('ADMIN')"/> 3 </http>
37.3 @AuthenticationPrincipal
Spring Security提供了AuthenticationPrincipalArgumentResolver,它可以为Spring MVC参数自动解析当前的Authentication.getPrincipal() 。通过使用@EnableWebSecurity,您将自动将它添加到您的Spring MVC配置中。如果您使用基于XML的配置,您必须自己添加。例如:
1 <mvc:annotation-driven> 2 <mvc:argument-resolvers> 3 <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" /> 4 </mvc:argument-resolvers> 5 </mvc:annotation-driven>
一旦正确配置了AuthenticationPrincipalArgumentResolver,您就可以在您的Spring MVC层中与Spring Security完全分离。
考虑这样一种情况:自定义用户详细信息服务UserDetailsService 返回一个实现用户详细信息UserDetails的对象和您自己的自定义用户CustomUser 对象。可以使用以下代码访问当前经过身份验证的用户的自定义用户CustomUser
:
1 @RequestMapping("/messages/inbox") 2 public ModelAndView findMessagesForUser() { 3 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 5 CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal(); 6 7 // .. find messages for this user and return them ... 8 }
从Spring Security 3.2开始,我们可以通过添加注释来更直接地解决这个问题。例如:
1 import org.springframework.security.core.annotation.AuthenticationPrincipal; 2 3 // ... 4 5 @RequestMapping("/messages/inbox") 6 public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) { 7 8 // .. find messages for this user and return them ... 9 }
有时可能有必要以某种方式改变主体。例如,如果CustomUser需要成为最终用户,它就不能被扩展。在这种情况下,用户详细信息服务UserDetailsService 可能会返回一个实现用户详细信息UserDetails的对象,并提供一个名为getCustomUser的方法来访问自定义用户CustomUser。例如,它可能看起来像:
1 public class CustomUserUserDetails extends User { 2 // ... 3 public CustomUser getCustomUser() { 4 return customUser; 5 } 6 }
然后,我们可以使用Authentication. getPrincipal()作为根对象的SpEL表达式来访问自定义用户CustomUser :
1 import org.springframework.security.core.annotation.AuthenticationPrincipal; 2 3 // ... 4 5 @RequestMapping("/messages/inbox") 6 public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) { 7 8 // .. find messags for this user and return them ... 9 }
我们也可以在我们的SpEL表达式中引用Beans。例如,如果我们使用JPA来管理我们的用户,并且我们想要修改和保存当前用户的属性,那么可以使用以下内容。
1 import org.springframework.security.core.annotation.AuthenticationPrincipal; 2 3 // ... 4 5 @PutMapping("/users/self") 6 public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser, 7 @RequestParam String firstName) { 8 9 // change the firstName on an attached instance which will be persisted to the database 10 attachedCustomUser.setFirstName(firstName); 11 12 // ... 13 }
通过将@AuthenticationPrincipal作为我们自己的注释的元注释,我们可以进一步消除对Spring Security的依赖。下面我们演示如何在名为@CurrentUser的注释上实现这一点。
重要的是要认识到,为了消除对Spring Security的依赖,消费应用程序会创建@CurrentUser。这一步不是严格要求的,但是有助于将您对Spring Security的依赖隔离到一个更中心的位置。
1 @Target({ElementType.PARAMETER, ElementType.TYPE}) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 @AuthenticationPrincipal 5 public @interface CurrentUser {}
既然已经指定了@CurrentUser,我们就可以用它来发出信号,将我们的自定义用户CustomUser
解析为当前经过身份验证的用户。我们还将对Spring Security的依赖隔离到一个文件中。
1 @RequestMapping("/messages/inbox") 2 public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) { 3 4 // .. find messages for this user and return them ... 5 }
37.4 Spring MVC Async Integration
Spring Web MVC 3.2+对异步请求处理有很好的支持。在没有额外配置的情况下,Spring Security会自动将SecurityContext设置为执行控制器返回的可调用的线程。例如,以下方法将自动使用创建可调用文件时可用的安全上下文执行其可调用文件:
1 @RequestMapping(method=RequestMethod.POST) 2 public Callable<String> processUpload(final MultipartFile file) { 3 4 return new Callable<String>() { 5 public Object call() throws Exception { 6 // ... 7 return "someView"; 8 } 9 }; 10 }
从技术上讲,Spring Security与WebAsyncManager集成在一起。用于处理可调用的安全上下文SecurityContext
是在调用startCallableProcessing时存在于安全上下文持有者SecurityContextHolder
上的安全上下文SecurityContext。
没有与控制器返回的延迟结果的自动集成。这是因为延迟结果是由用户处理的,因此无法自动与之集成。但是,您仍然可以使用并发支持来提供与Spring Security的透明集成。
37.5 Spring MVC and CSRF Integration
37.5.1 Automatic Token Inclusion
Spring Security将自动在使用Spring MVC表单标签的表单中包含CSRF令牌。例如,下面的JSP:
1 <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" 2 xmlns:c="http://java.sun.com/jsp/jstl/core" 3 xmlns:form="http://www.springframework.org/tags/form" version="2.0"> 4 <jsp:directive.page language="java" contentType="text/html" /> 5 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> 6 <!-- ... --> 7 8 <c:url var="logoutUrl" value="/logout"/> 9 <form:form action="${logoutUrl}" 10 method="post"> 11 <input type="submit" 12 value="Log out" /> 13 <input type="hidden" 14 name="${_csrf.parameterName}" 15 value="${_csrf.token}"/> 16 </form:form> 17 18 <!-- ... --> 19 </html> 20 </jsp:root>
将输出类似如下的超文本标记语言:
1 <!-- ... --> 2 3 <form action="/context/logout" method="post"> 4 <input type="submit" value="Log out"/> 5 <input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/> 6 </form> 7 8 <!-- ... -->
37.5.2 Resolving the CsrfToken
Spring Security提供了CsrfTokenArgumentResolver,它可以自动为Spring MVC参数解析当前的CsrfToken。通过使用@EnableWebSecurity,您将自动将它添加到您的Spring MVC配置中。如果您使用基于XML的配置,您必须自己添加。
一旦正确配置了CsrfTokenArgumentResolver,就可以将CsrfToken公开给基于静态HTML的应用程序。
1 @RestController 2 public class CsrfController { 3 4 @RequestMapping("/csrf") 5 public CsrfToken csrf(CsrfToken token) { 6 return token; 7 } 8 }
对CsrfToken保密对其他域来说很重要。这意味着,如果您正在使用跨域共享(CORS),您不应该向任何外部域公开CsrfToken。