springSecurity源码浅析

Spring-security概述

整体来讲,spring-security就是一个过滤器链filter-chain,由多个filter共同完成整体的登出,认证,鉴权功能。部分filter会根据配置动态的删除或增加,比如basicAuthenticationFilter.

整个过滤器链作为一个filter加入整体应用的过滤器链。

security-filter-chain

这是未加入任何配置spring-security所生成的filter-chain。

他们对应在整体过滤器链中的位置

对springsecurity定制开发的本质就是修改部分过滤器可替换组件,以及通过在spring-security过滤器链中添加自己的filter.

源码解析

springSecurity比较重和常见修改使用的过滤器为 LogoutFilter(登出控制),UsernamePasswordAuthenticationFilter(认证逻辑filter),FilterSecurityInterceptor(鉴权)。下面分别通过大致源码来梳理下他们的代码逻辑。

本文档内容所有handler都可以通过配置进行替换修改为自己的handler

LogoutFilter

登出Filter,有三个参数如下:

    //path匹配,用于判断是否是登出请求
    private RequestMatcher logoutRequestMatcher;
    //登出处理,固定为CompositeLogoutHandler,CompositeLogoutHandler包含多个LogoutHandler
    //内部LogoutHandler可自定义,执行登出逻辑时会循环执行所有LogoutHandler.logout方法。
    private final LogoutHandler handler;
    //登出成功处理Handler,可自定义,常见修改就是自定义登出接口。
    private final LogoutSuccessHandler logoutSuccessHandler;

源代码逻辑解析:

因为这部分很简单就直接贴过来了。

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //判断是否是登出请求
        if (this.requiresLogout(request, response)) {
            //从SecurityContextHolder中获取用户信息
            Authentication auth = SecurityContextHolder中获取用户信息.getContext().getAuthentication();
            //CompositeLogoutHandler处理,它会将所有LogoutHandler的logout方法执行一遍。
            this.handler.logout(request, response, auth);
            //执行登出成功逻辑。默认为SimpleUrlLogoutSuccessHandler,登出成功重定向到登陆页面。
            this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
        } else {
            chain.doFilter(request, response);
        }
    }

UsernamePasswordAuthenticationFilter

认证逻辑Filter,非常重要的Filter.

UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter。实现了attemptAuthentication。

首先是AbstractAuthenticationProcessingFilter的整体doFilter逻辑

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //判断是否是登录请求
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
                //具体filter实现的attemptAuthentication
                Authentication authenticationResult = this.attemptAuthentication(request, response);
                if (authenticationResult == null) {
                    return;
                }
                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }
			  //登陆成功,默认会执行successHandler的onAuthenticationSuccess方法
                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
                this.unsuccessfulAuthentication(request, response, var5);
            } catch (AuthenticationException var6) {
                //这里捕获的AuthenticationException异常,所以一般在登录失败逻辑中抛出此类型异常,然后
                //执行unsuccessfulAuthentication。默认为执行failureHandler.onAuthenticationFailure
                this.unsuccessfulAuthentication(request, response, var6);
            }

        }
    }

然后具体看下UsernamePasswordAuthenticationFilterattemptAuthentication方法

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

整体流程很清晰,就是拿到username,password然后交给AuthenticationManager处理。

默认实现为ProviderManager,具体代码

//包含一个AuthenticationProvider数组
private List<AuthenticationProvider数组> providers;

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...
        while(var9.hasNext()) {
            //循环调用AuthenticationProvider,
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            //看是否支持当前类型
            if (provider.supports(toTest)) {
                ...
                try {
           			//这里对所有的AuthenticationException异常都先进行捕获,在所有provider执行完,再进行处理
                    //这里就是多个provider只要有一个对这个信息认证成功了,就是成功了。
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
        		...
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }
        ...
        if (result != null) {
           ...
            return result;
        } else {
           // 如果不成功,对异常的处理
        }
    }

总结:登录整体流程 filter拦截登录请求->manager的authenticate方法–>provider的authenticate方法。

如果我要自定义一套自己的登录逻辑可以怎么做。

第一种可以只增加provider,因为只要有一个provider认证成功即为成功。

第二种就是替换自己的manger

第三种就是在UsernamePasswordAuthenticationFilter前增加自己的登录filter.目前koca是通过自己的filter实现多种登录拦截

FilterSecurityInterceptor 鉴权

鉴权虽然名字叫做Interceptor 但其实他还是实现了Filter接口

FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter 

dofilter方法只是包装FilterInvocation类执行invoke方法。

    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        //防止重复鉴权
        if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } else {
            if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
                filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
			//都是执行的父类方法,beforeInvocation是具体的鉴权逻辑
            InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

            try {
                filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }
    }

在父方法beforeInvocation调用了attemptAuthorization方法。

    private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
        try {
            //使用accessDecisionManager.decide鉴权
            this.accessDecisionManager.decide(authenticated, object, attributes);
        } catch (AccessDeniedException var5) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager));
            } else if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
            }

            this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5));    
            //不成功抛出异常。
            throw var5;
        }
    }

补充一点,校验不成功抛出的AccessDeniedException异常是谁处理的呢。

这之前的一个filter,ExceptionTranslationFilter的doFilter逻辑

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //通过try catch去捕获后续filter的异常
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            //只获取AuthenticationException和AccessDeniedException异常,其余的直接抛
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }
		//直接抛
            if (securityException == null) {
                this.rethrow(var8);
            }
			...
             //根据类型处理异常,最后通过accessDeniedHandler进行处理
            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }


koca安全认证功能与实现

  • 登录功能

    通过在UsernamePassword之前配置新的认证Filter,包含Filter,manager,provider。

    包含认证成功以及失败处理handler

  • token功能

    校验是添加Filter拦截器实现。

    生成是通过认证成功handler处理。

  • 第三方应用授权

    也是通过Filter添加实现

  • 鉴权

    是通过替换FilterSecurityInterceptor实现自己的鉴权逻辑

  • 其余功能

    包含异常处理handler等,