在spring的安全过滤器链中ExceptionTranslationFilter在FilterSecurityInterceptor的前面,这个过滤器自身并不执行具体的安全防护,他主要处理FilterSecurityInterceptor这个过滤器抛出的各种异常,并返回给客户端一个合适的http响应。
一、ExceptionTranslationFilter功能和属性
1.在类中主要有以下几个属性
- AuthenticationEntryPoint-处理需要重新认证的逻辑
- AccessDeniedHandler-处理请求被拒绝的逻辑
- AuthenticationTrustResolver-判断处理类型
- RequestCache-缓存客户端请求,当认证完成后能继续执行引发认证的原始请求
2.这个过滤器的主要逻辑在handleSpringSecurityException方法中,步骤如下
- 如果捕获到的异常是AuthenticationException,就重新执行认证
- 如果捕获到的异常是AccessDeniedException,再进一步执行下面的判断 ■ 如果当前的认证形式是Anonymous或者RememberMe,则重新执行认证
■ 否则就是当前认证用户没有权限访问被请求资源,调用accessDeniedHandler.handle方法
下面看下重新认证的方法sendStartAuthentication
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); requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); authenticationEntryPoint.commence(request, response, reason); }
首先会将现在SecurityContextHolder中的认证对象清除,之后将当前的调用保存在requestCache中,以便后面认证成功后能继续进行当前的处理,接着就调用authenticationEntryPoint的commence方法,将用户重定向到认证界面,提示用户进行认证。
3. AuthenticationEntryPoint类
这个类的主要作用是呈现给用户一个合适的响应从而提示用户能够重新登录,常见的实现类有
- BasicAuthenticationEntryPoint-基本认证对应的EntryPoint,修改Response的header,返回用户一个401码
- CasAuthenticationEntryPoint-cas认证对应的EntryPoint,将用户重定向到Cas服务器的登录页面
- LoginUrlAuthenticationEntryPoint-form login认证对应的EntryPoint,重定向到登录页面
- DelegatingAuthenticationEntryPoint-当系统中有多个EntryPoint,每个EntryPoint对应各自的匹配路径时采用这个EntryPoint。这个类维护一个RequestMatcher和EntryPoint对应关系的hash表,根据传入的request找到匹配的EntryPoint执行对应的commence方法,这个类里面还有一个defaultEntryPoint,当所有的EntryPoint都不匹配当前请求时调用默认EntryPoint的commence方法。
下面以LoginUrlAuthenticationEntryPoint为例看下代码实现
/** * Performs the redirect (or forward) to the login form URL. */ 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); 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); }
简单说明如下
- 当配置采用forward形式时,如果配置的scheme必须是https,而当前发起的请求是http时,先发一个重定向,和当前url一样仅仅把http修改成https,这样会重新抛出认证异常,重新进入到这个方法里,此时请求已变成https了
- 之后获取loginFormUrl,直接forward到这个对应的界面
- 如果配置的是不采用forward,则构建一个redirect到登录页面的url,之后直接重定向到这个界面
4.AccessDeniedHandler类
这个类主要用来处理当用户进行了认证,当时没有权限方法当前的资源而出现访问拒绝的情况。在正常使用场景下这种状况是不应该发生,因为我们的应用应该只提供能操作的UI给用户,比如一个供admin访问的页面应该对没有admin权限的用户隐藏,但是我们的安全策略不能只是依赖隐藏链接的方式,因为恶意的用户可以直接输入URL或者通过修改RESTful URL的参数等方式来绕过这个限制,在这个场合下就会出现AccessDeniedException异常。
当出现AccessDeniedException异常时,ExceptionTranslationFilter就会委托AccessDeniedHandler来进行处理,默认情况下AccessDeniedHandlerImpl会被调用,向客户端返回一个403码,我们可以通过配置AccessDeniedHandlerImpl的errorpage属性,从而向用户展示一个友好的页面(重新login页面),也可以通过自己实现AccessDeniedHandler提供定制化的服务。
5.SavedRequest和RequestCache类
ExceptionTranslationFilter类的另外一个功能就是在调用AuthenticationEntryPoint之前把当前的请求保存起来,默认利用HttpSessionRequestCache保存在session中,从而在认证完成后能重新执行当前的处理。如UsernamePasswordAuthenticationFilter和CasAuthenticationFilter在认证成功后通过调用SavedRequestAwareAuthenticationSuccessHandler这个类的onAuthenticationSuccess方法里,利用HttpSessionRequestCache从session中获取保存过的请求信息后,获取对应的url,发送一个重定向继续之前请求,RequestCacheAwareFilter类再将当前request换成保存的request从而恢复之前请求的完整信息。
二、.在spring boot环境下,采用Java config机制这个filter是如何追加到servlet中对我们的请求进行拦截的呢
在前面的章节 (spring-security(十六)Filter配置原理)中,我们知道spring 安全相关的Filter是在WebSecurity的build方法中调用HttpSecurity的build来将追加到HttpSecurity中filter列表排好序后构建成SecurityFilterChain,再把所有的SecurityFilterChain追加到FilterChainProxy中,最后通过DelegatingFilterProxy注册到ServletContext中的,下面我们主要来看下这个类是如何追加到HttpSecuriy的filter列表中的,以及对应的主要属性是如何配置的。
1. 从我们的配置入口WebSecurityConfigurerAdapter类开始,在这个类的getHttp()方法中,采用默认配置时会调用
http.exceptionHandling()方法,在这个方法中创建了一个实现了SecurityConfigurer接口的配置类ExceptionHandlingConfigurer,通过调用getOrApply方法最终追加到HttpSecurity的configurers属性中,通过这个配置类我们也可以设置ExceptionTranslationFilter中的authenticationEntryPoint、accessDeniedHandler等属性。
2. WebSecurity在构建HttpSecurity时,会调用HttpSecurity的build方法,这个方法会先执行HttpSecurity的configure()方法,就是依次调用configurers属性中各个SecurityConfigurer的configure方法
private void configure() throws Exception { Collection> configurers = getConfigurers(); for (SecurityConfigurer configurer : configurers) { configurer.configure((B) this); } } private Collection > getConfigurers() { List > result = new ArrayList >(); for (List > configs : this.configurers.values()) { result.addAll(configs); } return result; }
3. 下面来看下ExceptionHandlingConfigurer的configure方法
public void configure(H http) throws Exception { AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http); ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, getRequestCache(http)); if (accessDeniedHandler != null) { exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler); } exceptionTranslationFilter = postProcess(exceptionTranslationFilter); http.addFilter(exceptionTranslationFilter); }
可以看到,在这个类里面会创建一个Filter,并追加到HttpSecurity的filter列表中。
这样就可以明确看出我们的ExceptionTranslationFilter被加入到了httpsecurity的filter列表中了,下面我们看下这个filter中几个主要属性是怎么设置的
首先是创建entryPoint的getAuthenticationEntryPoint方法
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) { AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint; if (entryPoint == null) { entryPoint = createDefaultEntryPoint(http); } return entryPoint; } private AuthenticationEntryPoint createDefaultEntryPoint(H http) { if (defaultEntryPointMappings.isEmpty()) { return new Http403ForbiddenEntryPoint(); } if (defaultEntryPointMappings.size() == 1) { return defaultEntryPointMappings.values().iterator().next(); } DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(defaultEntryPointMappings); entryPoint.setDefaultEntryPoint(defaultEntryPointMappings.values().iterator().next()); return entryPoint; }
因为默认配置下我们没有给this.authenticationEntryPoint赋值,所以会调用createDefaultEntryPoint(H http)来获取,那这个地方的defaultEntryPointMappings是怎么设置的呢,为了说明这个属性,我们需要额外的看一些配置类。
例如当我们启用formLogin功能时,通过http.formLogin()配置,此时会向http的configurers列表中追加一个FormLoginConfigurer类,因为在httpsecurity的build方法中会先执行各个configure的init方法,我们下面看下FormLoginConfigurer的init方法就明白了,具体代码在FormLoginConfigurer的父类AbstractAuthenticationFilterConfigurer里
public void init(B http) throws Exception { updateAuthenticationDefaults(); if (permitAll) { PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); } registerDefaultAuthenticationEntryPoint(http); } private void registerDefaultAuthenticationEntryPoint(B http) { ExceptionHandlingConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } ContentNegotiationStrategy contentNegotiationStrategy = http .getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); } MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher( contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); RequestMatcher preferredMatcher = new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); exceptionHandling.defaultAuthenticationEntryPointFor( postProcess(authenticationEntryPoint), preferredMatcher); }
这样就可以明确看到通过获取到ExceptionHandlingConfigurer对象,并调用defaultAuthenticationEntryPointFor这个方法,就可以把formLogin对应的entrypoint追加到ExceptionHandlingConfigurer配置类中,并且设置了匹配规则。相似的HttpBasicConfigurer也是通过这种形式将基本认证对应的entrypoint追加进来的,当采用cas认证时,spring 没有为我们提供类似的配置类,但是我们可以直接调用下面的方法来设置
http.exceptionHandling().authenticationEntryPoint(casEntryPoint())
另外两个属性accessDeniedHandler和RequestCache的设置就比较简单了
如果配置类中指定了accessDeniedHandler就用,否则就不设置,ExceptionTranslationFilter类中默认使用AccessDeniedHandlerImpl这个实现类,requestCache的设置也比较简单
private RequestCache getRequestCache(H http) { RequestCache result = http.getSharedObject(RequestCache.class); if (result != null) { return result; } return new HttpSessionRequestCache(); }
如果指定就用指定的否则默认采用HttpSessionRequestCache。
这样我们的ExceptionTranslationFilter就完全组装好了,并且也作为Filter追加到了servlet中,可以对在鉴权过程中异常进行处理了