An in depth analysis of Spring Security cache request
- 2021-07-18 08:07:01
- OfStack
Why cache?
In order to better describe the problem, we take the website using form authentication as an example. The simplified authentication process is divided into 7 steps:
The user visits the website and opens a link (origin url). The request is sent to the server, which determines that the user requested the protected resource. Because the user is not logged in, the server redirects to the login page Fill out the form and click Login The browser sends the user name and password to the server in the form of a form The server verifies the username and password. Success, go to the next step. Otherwise, the user is required to re-authenticate (Step 3) Server to the user has the authority (role) judgment: has the authority, redirects to origin url; Insufficient permissions, return status code 403 ("forbidden").From Step 3, we can know that the user's request was interrupted.
After the user logs in successfully (step 7), it is redirected to origin url, and spring security enables the interrupted request to continue by using the cached request.
Using cache
After the user logs in successfully, the page is redirected to origin url. The request from the browser is first intercepted by the interceptor RequestCacheAwareFilter, and RequestCacheAwareFilter realizes the recovery of request through the RequestCache object held by RequestCacheAwareFilter.
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// request Match, then fetch, which will also change the cached request From session Delete in
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
// Preferential use of cached request
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
When to cache
First, we need to know about RequestCache and ExceptionTranslationFilter.
RequestCache
The RequestCache interface declares caching and recovery operations. The default implementation class is HttpSessionRequestCache. The implementation of HttpSessionRequestCache is relatively simple, and only the declaration of the interface is listed here:
public interface RequestCache {
// Will request Cache to session Medium
void saveRequest(HttpServletRequest request, HttpServletResponse response);
// From session Take from the middle request
SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
// Obtain and current request Matches the cache, and sets the matching cache request From session Delete in
HttpServletRequest getMatchingRequest(HttpServletRequest request,
HttpServletResponse response);
// Delete the cached request
void removeRequest(HttpServletRequest request, HttpServletResponse response);
}
ExceptionTranslationFilter
ExceptionTranslationFilter is one of the core filter of Spring Security, which is used to handle AuthenticationException and AccessDeniedException exceptions.
In our example, AuthenticationException refers to accessing protected resources without logging in, and AccessDeniedException refers to logging in but due to insufficient permissions (such as ordinary users accessing the administrator interface).
ExceptionTranslationFilter holds two processing classes, AuthenticationEntryPoint and AccessDeniedHandler.
ExceptionTranslationFilter handles exceptions through these two handling classes, and the handling rules are simple:
Rule 1. If the exception is AuthenticationException, use AuthenticationEntryPoint to handle Rule 2. If the exception is AccessDeniedException and the user is anonymous, use AuthenticationEntryPoint to handle Rule 3. If the exception is an AccessDeniedException and the user is not an anonymous user, leave it to AccessDeniedHandler if not.Corresponds to the following code
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);
}
}
}
The default implementation of AccessDeniedHandler is AccessDeniedHandlerImpl. This class handles exceptions by returning 403 error codes.
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
if (errorPage != null) { // Defines the errorPage
// errorPage You can manipulate the exception in
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
// Settings 403 Status code
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// Forward to errorPage
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else { // There is no definition errorPage Is returned 403 Status code (Forbidden) And error messages
response.sendError(HttpServletResponse.SC_FORBIDDEN,
accessDeniedException.getMessage());
}
}
}
The default implementation of AuthenticationEntryPoint is LoginUrlAuthenticationEntryPoint, and the processing of this class is forwarding or redirecting to the login page
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);
// Forwarding
dispatcher.forward(request, response);
return;
}
}
else {
// redirect to login page. Use https if forceHttps true
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
// Redirect
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
After understanding this, go back to our example.
In Step 3, ExceptionTranslationFilter catches an AuthenticationException exception when the user accesses a protected resource without logging in (Rule 1). The page needs to jump, and ExceptionTranslationFilter uses requestCache to cache request before jumping.
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);
// Cache request
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
1 Some pits
During development, if you don't understand how Spring Security caches request, you may step into a hole.
To give a simple example, if the website authentication is that the information is stored in header. When a protected resource is requested for the first time, the authentication information is not contained in the request header. If the authentication fails, the request will be cached. After that, even if the user fills in the information, the authentication will fail because the information is lost due to the recovery of request (see here for the description of the problem).
The simplest solution is, of course, not to cache request.
spring security provides NullRequestCache, which implements the RequestCache interface but does not operate.
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;
}
}
To configure requestCache, use the following code:
http.requestCache().requestCache(new NullRequestCache());
Supplement
By default, the three request are not cached.
The request address ends with/favicon. ico The content-type value in header is application/json The X-Requested-With value in header is XMLHttpRequestSee the private method createDefaultSavedRequestMatcher in the RequestCacheConfigurer class.
Attached is the example code: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache