spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程

spring cloud security 是构建 spring cloud 微服务中可靠的安全管理模块,其中 HttpSecurity 配置可以定制每个应用自己的安全策略,在 HttpSecurity 配置中 authenticationEntryPoint 配置项是用于处理凭证错误或无对应权限访问的入口,这里探讨这个入口工作的过程。

配置

配置 authenticationEntryPoint 需要继承 WebSecurityConfigurerAdapter 类并重写 protected void configure(HttpSecurity httpSecurity) 方法

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class TheSecurityConfig extends WebSecurityConfigurerAdapter {
    private EntryPoint entryPoint;
    private AccessDenied accessDenied;
    public TheSecurityConfig(EntryPoint entryPoint, AccessDenied accessDenied){
        this.entryPoint = entryPoint;
        this.accessDenied = accessDenied;
    }
    @Bean
    public TokenAuthFilter tokenAuthFilter() throws Exception {
        TokenAuthFilter tokenAuthFilter = new TokenAuthFilter(authenticationManager());
        return tokenAuthFilter;
    }
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 拦截规则
            .and()
            .authorizeRequests()
            .anyRequest().authenticated()
            // 未授权处理
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(entryPoint)
            .accessDeniedHandler(accessDenied)
            .and()
            // 自定义 token 解析过滤器获取权限和角色信息
            .addFilter(tokenAuthFilter())
            .csrf().disable();
    }
}
复制代码

以上配置采用自定义的认证接口,故没有配置 formLogin 项,其中 EntryPoint AccessDenied TokenAuthFilter可大致为以下形式

@Component
public class EntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 通过 response 写入返回内容
    }
}
复制代码
@Component
public class AccessDenied implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 通过 response 写入返回内容
    }
}
复制代码
public class TokenAuthFilter extends BasicAuthenticationFilter {
    public TokenAuthFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("Authorization");
        UsernamePasswordAuthenticationToken authRequest = null;
        // 自定义解析 token 将解析成功的信息存入 authRequest
        // 形如 authRequest = new UsernamePasswordAuthenticationToken(username, token, authorities);
        // authorities 是 Collection<GrantedAuthority> 类型,存入 SimpleGrantedAuthority 类型数据,是该请求的 角色信息或权限信息
        if(authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request, response);
    }
}
复制代码
请求过滤过程

请求进入服务过程中,运行 ApplicationFilterChain 中的 internalDoFilter 方法,核心代码如下

private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException {
// Call the next filter if there is one
    if (pos < n) {
        // ...
        try {
            // ...
            if( Globals.IS_SECURITY_ENABLED ) {
                // ...
            } else {
                // 运行过滤器
                filter.doFilter(request, response, this);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            // 异常捕获与抛出
            throw e;
        } catch (Throwable e) {
            // ...
        }
        return;
    }
    // We fell off the end of the chain -- call the servlet instance
    try {
        // ...
        if ((request instanceof HttpServletRequest) &&
                (response instanceof HttpServletResponse) &&
                Globals.IS_SECURITY_ENABLED ) {
            // ...
        } else {
            // 过滤器运行完毕后进入服务处理
            servlet.service(request, response);
        }
    } catch (IOException | ServletException | RuntimeException e) {
        // 异常捕获与抛出
        throw e;
    } catch (Throwable e) {
        // ...
    } finally {
        // ...
    }
}
复制代码

ExceptionTranslationFilter 这个过滤器中,存在一个异常捕获器

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) {
    // ...
        if (securityException == null) {
            securityException = (AccessDeniedException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        }
        // ...
        handleSpringSecurityException(request, response, chain, securityException);
    }
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        // ...
    }
    else if (exception instanceof AccessDeniedException) {
        handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
    }
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
    // ...
    if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
        // ...
        sendStartAuthentication(request, response, chain,
            new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
    }
    else {
        // ...
        this.accessDeniedHandler.handle(request, response, exception);
    }
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
    // ...
    // 这里就是配置的 authenticationEntryPoint 触发位置
    this.authenticationEntryPoint.commence(request, response, reason);
}
复制代码

通过以上源码可知,如果抛出 AccessDeniedException 且没有被 ExceptionTranslationFilter 之后的过滤器捕获并处理的话,将会到达该过滤器的 sendStartAuthentication 处理方法并调用 authenticationEntryPoint 方法,该方法就是配置 httpSecurityauthenticationEntryPoint 的入口方法,如果没有配置该方法时,请求返回内容将为空。

异常抛出

FilterOrderRegistration 中在 ExceptionTranslationFilter 后注册了 FilterSecurityInterceptor 过滤器,该过滤器继承了 AbstractSecurityInterceptor 类,在其 doFilter 方法中,调用了 invoke 方法,这个方法里有如下代码:

// 检查该请求是否可以被放行,不被放行便抛出 AccessDeniedException 异常
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
    filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
    super.finallyInvocation(token);
}
复制代码

上述代码中如果抛出 AccessDeniedException 异常,因为没有捕获处理错误的方法,便会抛向上一个过滤器,如果我们不改变过滤器顺序的话,就会在 ExceptionTranslationFilter 中进行捕获和处理。 如果请求被放行,则 FilterSecurityInterceptor 中不会抛出异常,便会走到下一个过滤器直到 internalDoFilter 类中的 servlet.service(request, response); 方法。如果在请求的方法上加诸如 @PreAuthorize("hasAuthority('admin')") 的注解的话,如果没有 admin 权限,也会到 AbstractSecurityInterceptor 类中的 attemptAuthorization 方法中抛出 AccessDeniedException 错误,一样的,如果在 ExceptionTranslationFilter 之后的过滤器中没有捕获处理的话,也会走到 sendStartAuthentication 方法中进行处理。

自定义捕获过滤器

上文了解到,AccessDeniedException 会被 ExceptionTranslationFilter 捕获,但如果我们在 ExceptionTranslationFilter 过滤器后添加一个专门处理 AccessDeniedException 错误的过滤器,则不需要配置 AuthenticationEntryPoint 入口,这样的过滤器可以是如下形式:

public class SomeFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try{
        chain.doFilter(request, response);
    } catch (Exception e){
    if(e instanceof AccessDeniedException){
        // 通过 response 写入返回内容或其他处理
        }
    }
}
复制代码

相应的, httpSecurity 需要添加 .addFilterAfter(SomeFilter(), ExceptionTranslationFilter.class) 配置。这样,就能不使用 AuthenticationEntryPoint 入口了,但这是没有必要的,这里还是建议使用 AuthenticationEntryPoint 入口配置。这里提出的只是一种参考方法,或者,需要注意不能在 ExceptionTranslationFilter 后添加一个捕获 AccessDeniedException 异常的过滤器,这会导致 AuthenticationEntryPoint 入口方法失效,如果捕获也应继续向上抛出。

猜你喜欢

转载自juejin.im/post/7050071382041821197