37. Spring MVC 集成
Spring Security 提供了许多与 Spring MVC 的可选集成。本节将详细介绍集成。
37.1 @EnableWebMvcSecurity
Note
从 Spring Security 4.0 开始,不推荐使用@EnableWebMvcSecurity
。替换为@EnableWebSecurity
,它将根据 Classpath 确定添加 Spring MVC 功能。
要启用与 Spring MVC 的 Spring Security 集成,请在配置中添加@EnableWebSecurity
Comments。
Note
Spring Security 使用 Spring MVC 的WebMvcConfigurerAdapter提供配置。这意味着,如果您使用的是更高级的选项,例如直接与WebMvcConfigurationSupport
集成,那么您将需要手动提供 Spring Security 配置。
37.2 MvcRequestMatcher
Spring Security 提供了 Spring MVC 如何匹配带有MvcRequestMatcher
的 URL 的深度集成。这有助于确保您的安全规则与用于处理请求的逻辑相匹配。
为了使用MvcRequestMatcher
,您必须将 Spring Security Configuration 与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
s ApplicationContext
中。
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[] { "/" };
}
}
Note
始终建议通过匹配HttpServletRequest
和方法安全性来提供授权规则。
通过在HttpServletRequest
上进行匹配来提供授权规则是很好的,因为它发生在代码路径的早期,并且有助于减少attack surface。方法安全性可确保如果有人绕过了 Web 授权规则,则您的应用程序仍然受到保护。这就是所谓的深度防御
考虑一个 Map 如下的控制器:
@RequestMapping("/admin")
public String admin() {
如果我们想将对这种控制器方法的访问限制为 Management 员用户,则开发人员可以通过在HttpServletRequest
上匹配以下内容来提供授权规则:
protected configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN");
}
或 XML
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
无论采用哪种配置,URL /admin
都将要求经过身份验证的用户为 Management 员用户。但是,根据我们的 Spring MVC 配置,URL /admin.html
也将 Map 到我们的admin()
方法。另外,根据我们的 Spring MVC 配置,URL /admin/
也将 Map 到我们的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
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
37.3 @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
Object
的Object
。可以使用以下代码访问当前已认证用户的CustomUser
:
@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 ...
}
从 Spring Security 3.2 开始,我们可以通过添加 Comments 来更直接地解析参数。例如:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {
// .. find messages for this user and return them ...
}
有时可能需要以某种方式转换主体。例如,如果CustomUser
必须是最终的,则不能扩展。在这种情况下,UserDetailsService
可能会返回实现UserDetails
并提供名为getCustomUser
的方法来访问CustomUser
的Object
。例如,它可能看起来像:
public class CustomUserUserDetails extends User {
// ...
public CustomUser getCustomUser() {
return customUser;
}
}
然后,我们可以使用以Authentication.getPrincipal()
作为根对象的SpEL expression来访问CustomUser
:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {
// .. find messags for this user and return them ...
}
我们还可以在 SpEL 表达式中引用 Bean。例如,如果我们使用 JPA 来 Management 用户,并且想要修改并保存当前用户的属性,则可以使用以下内容。
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);
// ...
}
我们可以通过在自己的 Comments 上设置@AuthenticationPrincipal
作为元 Comments 来进一步消除对 Spring Security 的依赖。下面我们演示如何在名为@CurrentUser
的 Comments 上执行此操作。
Note
重要的是要意识到,为了消除对 Spring Security 的依赖,创建@CurrentUser
的是消耗用户的应用程序。并非严格要求执行此步骤,但是可以帮助您将对 Spring Security 的依赖隔离到更中央的位置。
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}
现在已经指定了@CurrentUser
,我们可以用它来通知解析当前已认证用户的CustomUser
。我们还将对 Spring Security 的依赖关系隔离到一个文件中。
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {
// .. find messages for this user and return them ...
}
37.4 Spring MVC 异步集成
Spring Web MVC 3.2 对异步请求处理提供了出色的支持。如果没有其他配置,Spring Security 将自动将SecurityContext
设置为Thread
,该Thread
执行您的控制器返回的Callable
。例如,以下方法将自动使用创建Callable
时可用的SecurityContext
执行其Callable
:
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public Object call() throws Exception {
// ...
return "someView";
}
};
}
Note
从技术上讲,Spring Security 与WebAsyncManager
集成。用于处理Callable
的SecurityContext
是在调用startCallableProcessing
时SecurityContextHolder
上存在的SecurityContext
。
没有与控制器返回的DeferredResult
自动集成。这是因为DeferredResult
由用户处理,因此无法自动与其集成。但是,您仍然可以使用Concurrency Support提供与 Spring Security 的透明集成。
37.5 Spring MVC 和 CSRF 集成
37.5.1 自动包含令牌
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>
<!-- ... -->
37.5.2 解析 CsrfToken
Spring Security 提供了CsrfTokenArgumentResolver
,可以自动为 Spring MVC 参数解析当前的CsrfToken
。通过使用@EnableWebSecurity,您将自动将其添加到您的 Spring MVC 配置中。如果使用基于 XML 的配置,则必须自己添加。
正确配置CsrfTokenArgumentResolver
之后,您就可以将CsrfToken
暴露给基于静态 HTML 的应用程序。
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
重要的是,要使CsrfToken
成为其他域的 Secret。这意味着,如果您使用的是跨源共享(CORS),则应 不要 将CsrfToken
公开给任何外部域。