1. 概述
在本教程中,我們將演示 Spring Security 如何允許我們控制我們的 HTTP 會話。
這種控制範圍從會話超時到啓用併發會話和其他高級安全配置。
2. 會話何時創建?
我們可以精確控制我們的會話何時創建以及 Spring Security 將如何與之交互:
- always – 如果不存在已存在的會話,則會話將始終被創建。
- ifRequired – 僅在需要時才創建會話(默認)。
- never – 框架不會自行創建會話,但如果已存在則會使用它。
- stateless – Spring Security 不會創建或使用會話。
<http create-session="ifRequired">...</http>
以下是 Java 配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
return http.build();
}
務必理解,此配置僅控制 Spring Security 的行為,而不是整個應用程序。 Spring Security 不會在我們指示它不這樣做時創建會話,但我們的應用程序可能會!
默認情況下,Spring Security 會在需要時創建會話 — 這是“ifRequired”。
對於 一個無狀態應用程序, “never” 選項將確保 Spring Security 本身不會創建任何會話。 但是,如果應用程序創建了會話,Spring Security 將會使用它。
最嚴格的會話創建選項是“stateless”,它是一個 保證 Spring Security 不會創建任何會話。
此選項在 Spring 3.1 中 引入,並且有效地會跳過 Spring Security 過濾器鏈中的部分內容——主要是一些與會話相關的部分,例如 HttpSessionSecurityContextRepository、SessionManagementFilter 和 RequestCacheFilter。
這些更嚴格的控制機制意味着 cookie 不會被使用,因此 每個請求都需要重新進行身份驗證。
這種無狀態架構與 REST API 和它們的無狀態約束非常契合。 它們也與 Basic 和 Digest 身份驗證等身份驗證機制非常有效。
3. 內部機制
在運行身份驗證過程之前,Spring Security 將運行一個過濾器,負責在請求之間存儲 Security Context。這個過濾器是 SecurityContextPersistenceFilter。
上下文將根據策略 HttpSessionSecurityContextRepository 默認存儲,該策略使用 HTTP Session 作為存儲。
對於嚴格的 create-session=”stateless” 屬性,該策略將被另一個策略替換——NullSecurityContextRepository,並且 不會創建或使用任何會話以保持上下文。
4. 併發會話控制
當一個已認證的用户嘗試再次 認證,應用程序可以以幾種方式處理該事件。它可以無效當前用户的活動會話,並使用新的會話重新認證用户,也可以允許兩個會話併發存在。
啓用併發 會話控制 支持的第一步是在 web.xml 中添加以下監聽器:
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
或者,我們可以將其定義為 Bean:
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
這對於確保 Spring Security 會話註冊器在會話被銷燬時被 通知至關重要。
為了允許同一用户同時擁有多個併發會話,應在 XML 配置中使用<session-management>元素:
<http ...>
<session-management>
<concurrency-control max-sessions="2" />
</session-management>
</http>
或者,我們可以通過 Java 配置完成此操作:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().maximumSessions(2)
}
5. 會話超時時間
5.1. 處理會話超時
在會話超時後,如果用户發送帶有 過期會話 ID
的請求,他們將被重定向到通過命名空間可配置的 URL:<session-management>
<concurrency-control expired-url="/sessionExpired.html" ... />
</session-management>
同樣,如果用户發送帶有未過期的但完全無效的會話 ID 的請求,他們也將被重定向到可配置的 URL:
<session-management invalid-session-url="/invalidSession.html">
...
</session-management>
以下是相應的 Java 配置:
http.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer
.expiredUrl("/sessionExpired")
.invalidSessionUrl("/invalidSession"));
5.2. 使用 Spring Boot 配置會話超時時間
我們可以使用屬性輕鬆配置嵌入服務器的會話超時時間:
server.servlet.session.timeout=15m
如果未指定時間單位,Spring 將假定為秒。
簡而言之,通過此配置,在無活動狀態 15 分鐘後會話將過期。在這一段時間後,會話將被視為無效。
如果我們的項目配置為使用 Tomcat,則需要記住它僅支持秒級精度,且最小值為 1 分鐘。這意味着如果指定超時值為 170s,例如,則會產生 2 分鐘的超時。
最後,值得注意的是,儘管 Spring Session 支持類似屬性用於此目的 (spring.session.timeout),但如果未指定該屬性,則會自動配置將回退到前面提到的屬性值。
6. 阻止使用 URL 參數進行會話跟蹤
將會話信息暴露在 URL 中是一個日益增長的 安全風險 (從 2007 年第七名到 2013 年第二名在 OWASP Top 10 列表中)。
從 Spring 3.0 開始,將 jsessionid 追加到 URL 的 URL 重寫邏輯現在可以通過在 <http> 命名空間中設置 disable-url-rewriting=”true” 來禁用。
或者,從 Servlet 3.0 開始,會話跟蹤機制也可以在 web.xml 中配置:
<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
以及程序化:
servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
這選擇在 cookie 中還是在 URL 參數中存儲 JSESSIONID。
7. 使用 Spring Security 防禦會話固定攻擊
該框架通過配置用户嘗試再次認證時,對現有會話的處理,來提供對典型會話固定攻擊的保護:
<session-management session-fixation-protection="migrateSession"> ...
以下是對應的 Java 配置:
http.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionFixation().migrateSession());
默認情況下,Spring Security 啓用了此保護 (“migrateSession“)。在認證時,會創建一個新的 HTTP 會話,舊的會話失效,並且舊會話的屬性被複制到新會話中。
如果這不是我們想要的結果,還有兩個其他的選項可用:
- 當“none” 設置時,原始會話不會失效。
- 當“newSession” 設置時,會創建一個沒有從舊會話中複製任何屬性的乾淨會話。
8. 安全會話 Cookie
接下來,我們將討論如何安全我們的會話 Cookie。
我們可以使用 httpOnly 標誌和 secure 標誌來安全我們的會話 Cookie:
- httpOnly: 如果為 true,則瀏覽器腳本將無法訪問 Cookie
- secure: 如果為 true,則 Cookie 只會通過 HTTPS 連接發送
我們可以將這些標誌設置為會話 Cookie 在 web.xml 中:
<session-config>
<session-timeout>1</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
</session-config>
此配置選項適用於 Java Servlet 3. 默認情況下,http-only 為 true,secure 為 false。
讓我們也看看相應的 Java 配置:
public class MainWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext sc) throws ServletException {
// ...
sc.getSessionCookieConfig().setHttpOnly(true);
sc.getSessionCookieConfig().setSecure(true);
}
}
如果我們在 Spring Boot 中使用,我們可以將這些標誌設置為在 application.properties 中:
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
最後,我們還可以手動通過使用 Filter 來實現:
public class SessionFilter implements Filter {
@Override
public void doFilter(
ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
Cookie[] allCookies = req.getCookies();
if (allCookies != null) {
Cookie session =
Arrays.stream(allCookies).filter(x -> x.getName().equals("JSESSIONID"))
.findFirst().orElse(null);
if (session != null) {
session.setHttpOnly(true);
session.setSecure(true);
res.addCookie(session);
}
}
chain.doFilter(req, res);
}
}
9. 工作中與會話
9.1. 會話範圍的 Bean
可以使用 會話 作用域定義 Bean,只需在 Web 上下文中聲明的 Bean 上使用 @Scope 註解:
@Component
@Scope("session")
public class Foo { .. }
或者使用 XML:
<bean id="foo" scope="session"/>
然後可以將該 Bean 注入到另一個 Bean 中:
@Autowired
private Foo theFoo;
Spring 將該新 Bean 綁定到 HTTP 會話的生命週期。
9.2. 將原始會話注入到控制器中
原始 HTTP 會話也可以直接注入到 控制器 方法中:
@RequestMapping(..)
public void fooMethod(HttpSession session) {
session.setAttribute(Constants.FOO, new Foo());
//...
Foo foo = (Foo) session.getAttribute(Constants.FOO);
}
9.3. 獲取原始會話
當前的 HTTP 會話也可以通過 原始 Servlet API 編程方式獲取:
ServletRequestAttributes attr = (ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes();
HttpSession session= attr.getRequest().getSession(true); // true == allow create
10. 結論
在本文中,我們討論了使用 Spring Security 管理會話。
此外,Spring Reference 包含一份關於會話管理的非常好的 常見問題解答。