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(HttpServletRequest,HttpServletResponse)

您可以使用 HttpServletRequest.authenticate(HttpServletRequest,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。以下範例輸出目前使用者的身份驗證

  • 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 結構描述

<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 之前,一旦提交 HttpServletResponseSecurityContextHolder 中的 SecurityContext 就會自動儲存。這可能會在非同步環境中造成問題。請考慮以下範例

  • 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 版起,Spring Security 已足夠聰明,不再在提交 HttpServletResponse 時自動儲存 SecurityContext,只要叫用 HttpServletRequest.startAsync() 即可。

Servlet 3.1+ 整合

以下章節說明 Spring Security 整合的 Servlet 3.1 方法。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中用於防範 Session Fixation 攻擊的預設方法。