深入淺析 Spring Security 緩存請(qǐng)求問題
為什么要緩存?
為了更好的描述問題,我們拿使用表單認(rèn)證的網(wǎng)站舉例,簡(jiǎn)化后的認(rèn)證過程分為7步:
- 用戶訪問網(wǎng)站,打開了一個(gè)鏈接(origin url)。
- 請(qǐng)求發(fā)送給服務(wù)器,服務(wù)器判斷用戶請(qǐng)求了受保護(hù)的資源。
- 由于用戶沒有登錄,服務(wù)器重定向到登錄頁面
- 填寫表單,點(diǎn)擊登錄
- 瀏覽器將用戶名密碼以表單形式發(fā)送給服務(wù)器
- 服務(wù)器驗(yàn)證用戶名密碼。成功,進(jìn)入到下一步。否則要求用戶重新認(rèn)證(第三步)
- 服務(wù)器對(duì)用戶擁有的權(quán)限(角色)判定: 有權(quán)限,重定向到origin url; 權(quán)限不足,返回狀態(tài)碼403("forbidden").
從第3步,我們可以知道,用戶的請(qǐng)求被中斷了。
用戶登錄成功后(第7步),會(huì)被重定向到origin url,spring security通過使用緩存的request,使得被中斷的請(qǐng)求能夠繼續(xù)執(zhí)行。
使用緩存
用戶登錄成功后,頁面重定向到origin url。瀏覽器發(fā)出的請(qǐng)求優(yōu)先被攔截器RequestCacheAwareFilter攔截,RequestCacheAwareFilter通過其持有的RequestCache對(duì)象實(shí)現(xiàn)request的恢復(fù)。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// request匹配,則取出,該操作同時(shí)會(huì)將緩存的request從session中刪除
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
// 優(yōu)先使用緩存的request
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
何時(shí)緩存
首先,我們需要了解下RequestCache以及ExceptionTranslationFilter。
RequestCache
RequestCache接口聲明了緩存與恢復(fù)操作。默認(rèn)實(shí)現(xiàn)類是HttpSessionRequestCache。HttpSessionRequestCache的實(shí)現(xiàn)比較簡(jiǎn)單,這里只列出接口的聲明:
public interface RequestCache {
// 將request緩存到session中
void saveRequest(HttpServletRequest request, HttpServletResponse response);
// 從session中取request
SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
// 獲得與當(dāng)前request匹配的緩存,并將匹配的request從session中刪除
HttpServletRequest getMatchingRequest(HttpServletRequest request,
HttpServletResponse response);
// 刪除緩存的request
void removeRequest(HttpServletRequest request, HttpServletResponse response);
}
ExceptionTranslationFilter
ExceptionTranslationFilter 是Spring Security的核心filter之一,用來處理AuthenticationException和AccessDeniedException兩種異常。
在我們的例子中,AuthenticationException指的是未登錄狀態(tài)下訪問受保護(hù)資源,AccessDeniedException指的是登陸了但是由于權(quán)限不足(比如普通用戶訪問管理員界面)。
ExceptionTranslationFilter 持有兩個(gè)處理類,分別是AuthenticationEntryPoint和AccessDeniedHandler。
ExceptionTranslationFilter 對(duì)異常的處理是通過這兩個(gè)處理類實(shí)現(xiàn)的,處理規(guī)則很簡(jiǎn)單:
- 規(guī)則1. 如果異常是 AuthenticationException,使用 AuthenticationEntryPoint 處理
- 規(guī)則2. 如果異常是 AccessDeniedException 且用戶是匿名用戶,使用 AuthenticationEntryPoint 處理
- 規(guī)則3. 如果異常是 AccessDeniedException 且用戶不是匿名用戶,如果否則交給 AccessDeniedHandler 處理。
對(duì)應(yīng)以下代碼
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
.getContext().getAuthentication())) {
logger.debug(
"Access is denied (user is anonymous); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
AccessDeniedHandler 默認(rèn)實(shí)現(xiàn)是 AccessDeniedHandlerImpl。該類對(duì)異常的處理是返回403錯(cuò)誤碼。
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
if (errorPage != null) { // 定義了errorPage
// errorPage中可以操作該異常
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
// 設(shè)置403狀態(tài)碼
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 轉(zhuǎn)發(fā)到errorPage
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else { // 沒有定義errorPage,則返回403狀態(tài)碼(Forbidden),以及錯(cuò)誤信息
response.sendError(HttpServletResponse.SC_FORBIDDEN,
accessDeniedException.getMessage());
}
}
}
AuthenticationEntryPoint 默認(rèn)實(shí)現(xiàn)是 LoginUrlAuthenticationEntryPoint, 該類的處理是轉(zhuǎn)發(fā)或重定向到登錄頁面
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
// 轉(zhuǎn)發(fā)
dispatcher.forward(request, response);
return;
}
}
else {
// redirect to login page. Use https if forceHttps true
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
// 重定向
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
了解完這些,回到我們的例子。
第3步時(shí),用戶未登錄的情況下訪問受保護(hù)資源,ExceptionTranslationFilter會(huì)捕獲到AuthenticationException異常(規(guī)則1)。頁面需要跳轉(zhuǎn),ExceptionTranslationFilter在跳轉(zhuǎn)前使用requestCache緩存request。
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
// 緩存 request
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
一些坑
在開發(fā)過程中,如果不理解Spring Security如何緩存request,可能會(huì)踩一些坑。
舉個(gè)簡(jiǎn)單例子,如果網(wǎng)站認(rèn)證是信息存放在header中。第一次請(qǐng)求受保護(hù)資源時(shí),請(qǐng)求頭中不包含認(rèn)證信息 ,驗(yàn)證失敗,該請(qǐng)求會(huì)被緩存,之后即使用戶填寫了信息,也會(huì)因?yàn)閞equest被恢復(fù)導(dǎo)致信息丟失從而認(rèn)證失敗(問題描述可以參見這里。
最簡(jiǎn)單的方案當(dāng)然是不緩存request。
spring security 提供了NullRequestCache, 該類實(shí)現(xiàn)了 RequestCache 接口,但是沒有任何操作。
public class NullRequestCache implements RequestCache {
public SavedRequest getRequest(HttpServletRequest request,
HttpServletResponse response) {
return null;
}
public void removeRequest(HttpServletRequest request, HttpServletResponse response) {
}
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
}
public HttpServletRequest getMatchingRequest(HttpServletRequest request,
HttpServletResponse response) {
return null;
}
}
配置requestCache,使用如下代碼即可:
http.requestCache().requestCache(new NullRequestCache());
補(bǔ)充
默認(rèn)情況下,三種request不會(huì)被緩存。
- 請(qǐng)求地址以/favicon.ico結(jié)尾
- header中的content-type值為application/json
- header中的X-Requested-With值為XMLHttpRequest
可以參見:RequestCacheConfigurer類中的私有方法createDefaultSavedRequestMatcher。
附上實(shí)例代碼: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache
以上所述是小編給大家介紹的Spring Security 緩存請(qǐng)求問題,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
Java生成10個(gè)1000以內(nèi)的隨機(jī)數(shù)并用消息框顯示數(shù)組內(nèi)容然后求和輸出
這篇文章主要介紹了Java生成10個(gè)1000以內(nèi)的隨機(jī)數(shù)并用消息框顯示數(shù)組內(nèi)容然后求和輸出,需要的朋友可以參考下2015-10-10
詳解Spring boot上配置與使用mybatis plus
這篇文章主要介紹了詳解Spring boot上配置與使用mybatis plus,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
SpringBoot使用MyBatis-Plus解決Invalid?bound?statement異常
這篇文章主要介紹了SpringBoot使用MyBatis-Plus解決Invalid?bound?statement異常,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
Kafka單機(jī)多broker實(shí)例集群搭建教程詳解
Apache?Kafka?是一個(gè)分布式流處理平臺(tái),廣泛應(yīng)用于日志收集、監(jiān)控?cái)?shù)據(jù)聚合等,本文將詳細(xì)介紹如何在一個(gè)單機(jī)上搭建多個(gè)Kafka?Broker實(shí)例的步驟,希望對(duì)大家有所幫助2025-03-03
Java實(shí)現(xiàn)計(jì)算一個(gè)月有多少天和多少周
這篇文章主要介紹了Java實(shí)現(xiàn)計(jì)算一個(gè)月有多少天和多少周,本文直接給出實(shí)例代碼,需要的朋友可以參考下2015-06-06
深入理解MyBatis中的一級(jí)緩存與二級(jí)緩存
這篇文章主要給大家深入的介紹了關(guān)于MyBatis中一級(jí)緩存與二級(jí)緩存的相關(guān)資料,文中詳細(xì)介紹MyBatis中一級(jí)緩存與二級(jí)緩存的工作原理及使用,對(duì)大家具有一定的參考性學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-06-06
Servlet關(guān)于RequestDispatcher的原理詳解
這篇文章主要介紹了Servlet關(guān)于RequestDispatcher的原理詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11

