Spring MVC 整合
Spring Security 提供了許多與 Spring MVC 的可選整合。本節將詳細介紹這些整合。
@EnableWebMvcSecurity
從 Spring Security 4.0 開始, |
要啟用 Spring Security 與 Spring MVC 的整合,請將 @EnableWebSecurity
註解新增到您的配置中。
Spring Security 使用 Spring MVC 的 |
MvcRequestMatcher
Spring Security 透過 MvcRequestMatcher
與 Spring MVC 的 URL 匹配方式進行了深度整合。這有助於確保您的安全規則與處理請求的邏輯一致。
要使用 MvcRequestMatcher
,必須將 Spring Security 配置放在與您的 DispatcherServlet
相同的 ApplicationContext
中。這是必要的,因為 Spring Security 的 MvcRequestMatcher
期望您的 Spring MVC 配置註冊一個名為 mvcHandlerMappingIntrospector
的 HandlerMappingIntrospector
bean,用於執行匹配。
對於 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("/")
}
}
我們始終建議您透過匹配 |
考慮一個控制器,其對映如下:
-
Java
-
Kotlin
@RequestMapping("/admin")
public String admin() {
// ...
}
@RequestMapping("/admin")
fun admin(): String {
// ...
}
要將此控制器方法的訪問限制為管理員使用者,可以透過以下方式匹配 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,它會自動建立一個 MvcRequestMatcher
。因此,它將使用 Spring MVC 匹配 URL 的方式來保護 Spring MVC 將匹配的相同 URL。
使用 Spring MVC 時一個常見的需求是指定 servlet path 屬性,為此您可以使用 MvcRequestMatcher.Builder
建立多個共享相同 servlet path 的 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
,它可以自動解析當前 Authentication.getPrincipal()
用於 Spring MVC 引數。透過使用 @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
。可以透過以下程式碼訪問當前認證使用者的 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
}
然後我們可以使用將 Authentication.getPrincipal()
作為根物件的 SpEL 表示式來訪問 CustomUser
。
-
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
已被指定,我們可以使用它來指示解析當前認證使用者的 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 ...
}
一旦它成為元註解,引數化也對您可用。
例如,考慮當您將 JWT 作為 principal 並且想要指定要檢索哪個 claim 時。作為元註解,您可以這樣做:
-
Java
-
Kotlin
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['sub']")
public @interface CurrentUser {}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal(expression = "claims['sub']")
annotation class CurrentUser
這已經相當強大了。但是,它也僅限於檢索 sub
claim。
為了使其更靈活,首先發布 AnnotationTemplateExpressionDefaults
bean,如下所示:
-
Java
-
Kotlin
-
Xml
@Bean
public AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDeafults();
}
@Bean
fun templateDefaults(): AnnotationTemplateExpressionDefaults {
return AnnotationTemplateExpressionDeafults()
}
<b:bean name="annotationExpressionTemplateDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/>
然後您可以像這樣為 @CurrentUser
提供一個引數:
-
Java
-
Kotlin
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims['{claim}']")
public @interface CurrentUser {
String claim() default 'sub';
}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@AuthenticationPrincipal(expression = "claims['{claim}']")
annotation class CurrentUser(val claim: String = "sub")
這將允許您在整個應用程式集中擁有更大的靈活性,如下所示:
-
Java
-
Kotlin
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser("user_id") String userId) {
// .. find messages for this user and return them ...
}
@RequestMapping("/messages/inbox")
open fun findMessagesForUser(@CurrentUser("user_id") userId: String?): ModelAndView {
// .. find messages for this user and return them ...
}
Spring MVC 非同步整合
Spring Web MVC 3.2+ 對非同步請求處理提供了很好的支援。無需額外配置,Spring Security 會自動將 SecurityContext
設定到呼叫控制器返回的 Callable
的 Thread
。例如,以下方法的 Callable
會自動使用建立 Callable
時可用的 SecurityContext
進行呼叫:
-
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"
}
}
將 SecurityContext 關聯到 Callable
更技術地講,Spring Security 整合到 |
與控制器返回的 DeferredResult
沒有自動整合。這是因為 DeferredResult
由使用者處理,因此無法自動與其整合。但是,您仍然可以使用併發支援來提供與 Spring Security 的透明整合。
Spring MVC 和 CSRF 整合
Spring Security 與 Spring MVC 整合以新增 CSRF 保護。
自動令牌包含
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>
<!-- ... -->
解析 CsrfToken
Spring Security 提供了 CsrfTokenArgumentResolver
,它可以自動解析當前 CsrfToken
用於 Spring MVC 引數。透過使用@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
保密,不讓其他域獲取,這一點非常重要。這意味著如果您使用跨域共享 (CORS),您不應將 CsrfToken
暴露給任何外部域。