org.springframework.security.web.access.ExceptionTranslationFilter Maven / Gradle / Ivy
/*
* Copyright 2004-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.access;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
/**
* Handles any AccessDeniedException
and AuthenticationException
* thrown within the filter chain.
*
* This filter is necessary because it provides the bridge between Java exceptions and
* HTTP responses. It is solely concerned with maintaining the user interface. This filter
* does not do any actual security enforcement.
*
* If an {@link AuthenticationException} is detected, the filter will launch the
* authenticationEntryPoint
. This allows common handling of authentication
* failures originating from any subclass of
* {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
*
* If an {@link AccessDeniedException} is detected, the filter will determine whether or
* not the user is an anonymous user. If they are an anonymous user, the
* authenticationEntryPoint
will be launched. If they are not an anonymous
* user, the filter will delegate to the
* {@link org.springframework.security.web.access.AccessDeniedHandler}. By default the
* filter will use
* {@link org.springframework.security.web.access.AccessDeniedHandlerImpl}.
*
* To use this filter, it is necessary to specify the following properties:
*
* authenticationEntryPoint
indicates the handler that should commence
* the authentication process if an AuthenticationException
is detected. Note
* that this may also switch the current protocol from http to https for an SSL
* login.
* - requestCache determines the strategy used to save a request during the
* authentication process in order that it may be retrieved and reused once the user has
* authenticated. The default implementation is {@link HttpSessionRequestCache}.
*
*
* @author Ben Alex
* @author colin sampaleanu
*/
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private final RequestCache requestCache;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationEntryPoint, new HttpSessionRequestCache());
}
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
@Override
public void afterPropertiesSet() {
Assert.notNull(this.authenticationEntryPoint, "authenticationEntryPoint must be specified");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void rethrow(Exception ex) throws ServletException {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return this.authenticationEntryPoint;
}
protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
return this.authenticationTrustResolver;
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
this.logger.trace("Sending to authentication entry point since authentication failed", exception);
sendStartAuthentication(request, response, chain, exception);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
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
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
this.accessDeniedHandler = accessDeniedHandler;
}
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null");
this.authenticationTrustResolver = authenticationTrustResolver;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
this.throwableAnalyzer = throwableAnalyzer;
}
/**
* @since 5.5
*/
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
*
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Default implementation of ThrowableAnalyzer
which is capable of also
* unwrapping ServletException
s.
*/
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
/**
* @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
*/
@Override
protected void initExtractorMap() {
super.initExtractorMap();
registerExtractor(ServletException.class, (throwable) -> {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
return ((ServletException) throwable).getRootCause();
});
}
}
}