# 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
中。
<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
sApplicationContext
中。
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<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(
RootConfiguration::class.java,
WebMvcConfiguration::class.java
)
}
override fun getServletMappings(): Array<String> {
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中
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</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。
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中
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
# @AuthenticationPrincipal
Spring 安全性提供了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 安全性完全解耦。
考虑一种情况,其中一个自定义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<out GrantedAuthority>?
) : User(username, password, authorities) {
// ...
val customUser: CustomUser? = null
}
然后,我们可以使用一个Spel表达式 (opens new window)访问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+对异步请求处理 (opens new window)具有出色的支持。 Spring 在没有额外配置的情况下,Security将自动将SecurityContext
设置为调用控制器返回的Thread
的Callable
。例如,下面的方法将自动使用创建Callable
时可用的SecurityContext
调用其Callable
:
Java
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public Object call() throws Exception {
// ...
return "someView";
}
};
}
Kotlin
@RequestMapping(method = [RequestMethod.POST])
open fun processUpload(file: MultipartFile?): Callable<String> {
return Callable {
// ...
"someView"
}
}
将SecurityContext与Callable的 更严格地说, Spring Security与 WebAsyncManager 集成。用于处理 Callable 的SecurityContext 是在调用SecurityContextHolder 时存在于SecurityContextHolder 上的SecurityContext 。 |
---|
没有控制器返回的DeferredResult
的自动集成。这是因为DeferredResult
是由用户处理的,因此无法自动与其集成。然而,你仍然可以使用并发支持来提供具有 Spring 安全性的透明集成。
# Spring MVC和CSRF集成
# 自动令牌包含
Spring 在使用Spring MVC form tag (opens new window)的窗体中,安全性将自动包括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
@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
是其他域的秘密是很重要的。这意味着如果你使用跨源共享 (opens new window),那么你应该NOT将CsrfToken
公开到任何外部域。