Servlet API 整合

Servlet 2.5+ 整合

本節介紹 Spring Security 如何與 Servlet 2.5 規範整合。

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser() 返回 SecurityContextHolder.getContext().getAuthentication().getName() 的結果,這通常是當前使用者名稱。如果您想在應用程式中顯示當前使用者名稱,這會很有用。此外,您可以檢查此返回值是否為 null,以確定使用者是否已認證或是否為匿名使用者。瞭解使用者是否已認證對於確定是否應顯示某些 UI 元素很有用(例如,僅當用戶已認證時才應顯示的登出連結)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal() 返回 SecurityContextHolder.getContext().getAuthentication() 的結果。這意味著它是一個 Authentication 物件,在使用基於使用者名稱和密碼的認證時,通常是 UsernamePasswordAuthenticationToken 的例項。如果您需要使用者的額外資訊,這會很有用。例如,您可能建立了一個自定義的 UserDetailsService,它返回包含使用者名稱字和姓氏的自定義 UserDetails。您可以透過以下方式獲取此資訊:

  • Java

  • Kotlin

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();
val auth: Authentication = httpServletRequest.getUserPrincipal()
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
val userDetails: MyCustomUserDetails = auth.principal as MyCustomUserDetails
val firstName: String = userDetails.firstName
val lastName: String = userDetails.lastName

應該注意的是,在應用程式中執行如此多邏輯通常不是一個好的實踐。相反,應該將其集中化以減少 Spring Security 和 Servlet API 的耦合。

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String) 用於確定 SecurityContextHolder.getContext().getAuthentication().getAuthorities() 是否包含傳入 isUserInRole(String) 方法的角色的 GrantedAuthority。通常,使用者不應該向此方法傳遞 ROLE_ 字首,因為它會自動新增。例如,如果您想確定當前使用者是否具有 "ROLE_ADMIN" 許可權,可以使用以下方式:

  • Java

  • Kotlin

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");
val isAdmin: Boolean = httpServletRequest.isUserInRole("ADMIN")

這對於確定是否應該顯示某些 UI 元件會很有用。例如,您可能僅在當前使用者是管理員時才顯示管理員連結。

Servlet 3+ 整合

以下部分描述了 Spring Security 整合的 Servlet 3 方法。

HttpServletRequest.authenticate(HttpServletResponse)

您可以使用 HttpServletRequest.authenticate(HttpServletResponse) 方法來確保使用者已認證。如果他們未認證,將使用配置的 AuthenticationEntryPoint 來請求使用者進行認證(重定向到登入頁面)。

HttpServletRequest.login(String,String)

您可以使用 HttpServletRequest.login(String,String) 方法使用當前的 AuthenticationManager 認證使用者。例如,以下程式碼將嘗試使用使用者名稱 user 和密碼 password 進行認證:

  • Java

  • Kotlin

try {
httpServletRequest.login("user","password");
} catch(ServletException ex) {
// fail to authenticate
}
try {
    httpServletRequest.login("user", "password")
} catch (ex: ServletException) {
    // fail to authenticate
}

如果您希望 Spring Security 處理失敗的認證嘗試,則無需捕獲 ServletException

HttpServletRequest.logout()

您可以使用 HttpServletRequest.logout() 方法登出當前使用者。

通常,這意味著 SecurityContextHolder 被清除,HttpSession 失效,所有“記住我”的認證都被清理等等。然而,配置的 LogoutHandler 實現會根據您的 Spring Security 配置而有所不同。請注意,在呼叫 HttpServletRequest.logout() 後,您仍然負責寫出響應。通常,這會涉及重定向到歡迎頁面。

AsyncContext.start(Runnable)

AsyncContext.start(Runnable) 方法確保您的憑據傳播到新的 Thread。透過使用 Spring Security 的併發支援,Spring Security 重寫了 AsyncContext.start(Runnable),以確保在處理 Runnable 時使用當前的 SecurityContext。以下示例輸出了當前使用者的 Authentication:

  • Java

  • Kotlin

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
	public void run() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		try {
			final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
			asyncResponse.setStatus(HttpServletResponse.SC_OK);
			asyncResponse.getWriter().write(String.valueOf(authentication));
			async.complete();
		} catch(Exception ex) {
			throw new RuntimeException(ex);
		}
	}
});
val async: AsyncContext = httpServletRequest.startAsync()
async.start {
    val authentication: Authentication = SecurityContextHolder.getContext().authentication
    try {
        val asyncResponse = async.response as HttpServletResponse
        asyncResponse.status = HttpServletResponse.SC_OK
        asyncResponse.writer.write(String.valueOf(authentication))
        async.complete()
    } catch (ex: Exception) {
        throw RuntimeException(ex)
    }
}

Async Servlet 支援

如果您使用基於 Java 的配置,則已準備就緒。如果您使用 XML 配置,則需要進行一些更新。第一步是確保您已將 web.xml 檔案更新為使用至少 3.0 的 schema:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

接下來,您需要確保您的 springSecurityFilterChain 已設定為處理非同步請求:

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
	org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

現在 Spring Security 確保您的 SecurityContext 也會在非同步請求中傳播。

那麼它是如何工作的呢?如果您不是真的感興趣,可以隨意跳過本節的其餘部分 大部分功能內置於 Servlet 規範中,但 Spring Security 做了一些調整,以確保非同步請求能夠正常工作。在 Spring Security 3.2 之前,SecurityContextHolder 中的 SecurityContextHttpServletResponse 提交時會自動儲存。這在非同步環境中可能會導致問題。考慮以下示例:

  • Java

  • Kotlin

httpServletRequest.startAsync();
new Thread("AsyncThread") {
	@Override
	public void run() {
		try {
			// Do work
			TimeUnit.SECONDS.sleep(1);

			// Write to and commit the httpServletResponse
			httpServletResponse.getOutputStream().flush();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}.start();
httpServletRequest.startAsync()
object : Thread("AsyncThread") {
    override fun run() {
        try {
            // Do work
            TimeUnit.SECONDS.sleep(1)

            // Write to and commit the httpServletResponse
            httpServletResponse.outputStream.flush()
        } catch (ex: java.lang.Exception) {
            ex.printStackTrace()
        }
    }
}.start()

問題在於 Spring Security 不知道這個 Thread,因此 SecurityContext 未傳播到它。這意味著,當我們提交 HttpServletResponse 時,沒有 SecurityContext。當 Spring Security 在提交 HttpServletResponse 時自動儲存 SecurityContext 時,它會丟失已登入的使用者。

從 3.2 版本開始,一旦呼叫 HttpServletRequest.startAsync(),Spring Security 足夠智慧,不再在提交 HttpServletResponse 時自動儲存 SecurityContext

Servlet 3.1+ 整合

以下部分描述了 Spring Security 整合的 Servlet 3.1 方法。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中,防禦 會話固定 (Session Fixation) 攻擊的預設方法。