Servlet API 整合
Servlet 2.5+ 整合
本節描述了 Spring Security 如何與 Servlet 2.5 規範整合。
HttpServletRequest.getRemoteUser()
HttpServletRequest.getRemoteUser() 返回 SecurityContextHolder.getContext().getAuthentication().getName() 的結果,這通常是當前使用者名稱。如果您想在應用程式中顯示當前使用者名稱,這會很有用。此外,您可以透過檢查此值是否為空來確定使用者是否已透過身份驗證或是否為匿名使用者。瞭解使用者是否已透過身份驗證對於確定是否應顯示某些 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() 是否包含一個 GrantedAuthority,其角色與傳遞給 isUserInRole(String) 的角色相同。通常,使用者不應將 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 處理失敗的身份驗證嘗試,則無需捕獲 |
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)
}
}
非同步 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 之前,只要 HttpServletResponse 被提交,SecurityContextHolder 中的 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 版本以來,一旦呼叫 HttpServletRequest.startAsync(),Spring Security 就會足夠智慧地不再在提交 HttpServletResponse 時自動儲存 SecurityContext。
Servlet 3.1+ 整合
以下部分描述了 Spring Security 整合的 Servlet 3.1 方法。
HttpServletRequest#changeSessionId()
HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中預設用於防範 會話固定 攻擊的方法。