Spring MVC 整合
Spring Security 提供了多項與 Spring MVC 的可選整合。本節將詳細介紹這些整合。
@EnableWebSecurity
要啟用 Spring Security 與 Spring MVC 的整合,請在配置中新增 @EnableWebSecurity 註解。
|
Spring Security 透過使用 Spring MVC 的 |
PathPatternRequestMatcher
Spring Security 透過 PathPatternRequestMatcher 與 Spring MVC 如何匹配 URL 進行深度整合。這有助於確保您的安全規則與處理請求的邏輯相匹配。
PathPatternRequestMatcher 必須使用與 Spring MVC 相同的 PathPatternParser。如果您沒有自定義 PathPatternParser,則可以執行以下操作
-
Java
-
Kotlin
-
Xml
@Bean
PathPatternRequestMatcherBuilderFactoryBean usePathPattern() {
return new PathPatternRequestMatcherBuilderFactoryBean();
}
@Bean
fun usePathPattern(): PathPatternRequestMatcherBuilderFactoryBean {
return PathPatternRequestMatcherBuilderFactoryBean()
}
<b:bean class="org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean"/>
然後 Spring Security 將為您找到合適的 Spring MVC 配置。
如果您正在自定義 Spring MVC 的 PathPatternParser 例項,則需要在同一個 ApplicationContext 中配置 Spring Security 和 Spring MVC。
|
我們始終建議您透過匹配 |
現在 Spring MVC 已與 Spring Security 整合,您已準備好編寫一些將使用 PathPatternRequestMatcher 的授權規則。
@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
-
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 ...
}
有時,您可能需要以某種方式轉換主體。例如,如果 CustomUser 需要是 final,它就不能被擴充套件。在這種情況下,UserDetailsService 可能會返回一個實現 UserDetails 並提供一個名為 getCustomUser 的方法來訪問 CustomUser 的 Object
-
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 作為您的主體並且您想說明要檢索哪個宣告時。作為元註解,您可以這樣做
-
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 宣告。
為了使其更靈活,首先發布 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,它可以自動解析 Spring MVC 引數的當前 CsrfToken。透過使用@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 暴露給任何外部域。
在同一個 Application Context 中配置 Spring MVC 和 Spring Security
如果您使用 Boot,Spring MVC 和 Spring Security 預設在同一個應用程式上下文中。
否則,對於 Java Config,同時包含 @EnableWebMvc 和 @EnableWebSecurity 將在同一個上下文中構建 Spring Security 和 Spring MVC 元件。
或者,如果您使用 ServletListener,您可以這樣做
-
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("/")
}
}
最後,對於 web.xml 檔案,您按如下方式配置 DispatcherServlet
<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 中。