• spring-security-4 (4)spring security 认证和授权原理


      在上一节我们讨论了spring security过滤器的创建和注册原理。请记住springSecurityFilterChain(类型为FilterChainProxy)是实际起作用的过滤器链,DelegatingFilterProxy起到代理作用。

      但是这还没有解决我们最初的所有问题,那就是虽然创建了springSecurityFilterChain过滤器链,那么过滤器链中的过滤器是如何一 一创建的?这些过滤器是如何实现认证和授权的?本节我们来讨论这个问题。

      注意:本节代码示例,采用的依然是spring-security-4 (2)spring security 基于Java配置的搭建中的代码为例。

    一、过滤器的创建

      我们创建的MySecurityConfig继承了WebSecurityConfigurerAdapter。WebSecurityConfigurerAdapter中有个configure(HttpSecurity http)的方法:

    protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests() //拦截请求,创建FilterSecurityInterceptor
                    .anyRequest().authenticated() //在创建过滤器的基础上的一些自定义配置
                    .and() //用and来表示配置过滤器结束,以便进行下一个过滤器的创建和配置
                .formLogin().and() //设置表单登录,创建UsernamePasswordAuthenticationFilter
                .httpBasic(); //basic验证,创建BasicAuthenticationFilter
    }

      该方法用来实现spring security的一些自定义的配置,其中就包括Filter的创建。其中http.authorizeRequests()、http.formLogin()、http.httpBasic()分别创建了ExpressionUrlAuthorizationConfigurer,FormLoginConfigurer,HttpBasicConfigurer。在三个类从父级一直往上找,会发现它们都是SecurityConfigurer的子类。SecurityConfigurer中又有configure方法。该方法被子类实现就用于创建各个过滤器,并将过滤器添加进HttpSecurity中维护的装有Filter的List中,比如HttpBasicConfigurer中的configure方法,源码如下:

    public void configure(B http) throws Exception {
            AuthenticationManager authenticationManager = http
                    .getSharedObject(AuthenticationManager.class);
            //创建BasicAuthenticationFilter过滤器
            BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(
                    authenticationManager, this.authenticationEntryPoint);
            if (this.authenticationDetailsSource != null) {
                basicAuthenticationFilter
                        .setAuthenticationDetailsSource(this.authenticationDetailsSource);
            }
            RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
            if(rememberMeServices != null) {
                basicAuthenticationFilter.setRememberMeServices(rememberMeServices);
            }
            basicAuthenticationFilter = postProcess(basicAuthenticationFilter);
            //添加过滤器
            http.addFilter(basicAuthenticationFilter);
    }

      另外,并非所有的过滤器都是在configure中进行创建的,比如UsernamePasswordAuthenticationFilter是在调用FormLoginConfigurer的构造方法时创建的。FormLoginConfigurer部分源码如下:

    public FormLoginConfigurer() {
            super(new UsernamePasswordAuthenticationFilter(), null);
            usernameParameter("username");
            passwordParameter("password");
    }

     HttpSecurity的父类是AbstractConfiguredSecurityBuilder,该类中有个configure方法用来获取所有SecurityConfigurer,并调用所有SecurityConfigurer的configure方法。源码如下:

    private void configure() throws Exception {
            //获取所有SecurityConfigurer类
            Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    
            for (SecurityConfigurer<O, B> configurer : configurers) {
                //调用所有SecurityConfigurer的configure方法
                configurer.configure((B) this);
            }
    }

       以上就是过滤器的创建过程。当我们的MySecurityConfig继承了WebSecurityConfigurerAdapter以后,就默认有了configure(HttpSecurity http)方法。我们也可以在MySecurityConfig中重写此方法来进行更灵活的配置。

    @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
            .authorizeRequests() //注册FilterSecurityInterceptor
                 .antMatchers("/index.html").permitAll()//访问index.html不要权限验证
                 .anyRequest().authenticated()//其他所有路径都需要权限校验
            .and()
                 .csrf().disable()//默认开启,可以显示关闭
            .formLogin()  //内部注册 UsernamePasswordAuthenticationFilter
                .loginPage("/login.html") //表单登录页面地址
                .loginProcessingUrl("/login")//form表单POST请求url提交地址,默认为/login
                .passwordParameter("password")//form表单用户名参数名
                .usernameParameter("username") //form表单密码参数名
                .successForwardUrl("/success.html")  //登录成功跳转地址
                .failureForwardUrl("/error.html") //登录失败跳转地址
                //.defaultSuccessUrl()//如果用户没有访问受保护的页面,默认跳转到页面
                //.failureUrl()
                //.failureHandler(AuthenticationFailureHandler)
                //.successHandler(AuthenticationSuccessHandler)
                //.failureUrl("/login?error")
                .permitAll();//允许所有用户都有权限访问loginPage,loginProcessingUrl,failureForwardUrl
        }

      虽然我们上面仅仅看到了三种过滤器的创建,但是真正创建的远不止三种,spring secuirty会默认帮我们注册一些过滤器。比如SecurityContextPersistenceFilter,该过滤器用于在我们请求到来时,将SecurityContext从Session中取出放入SecuirtyContextHolder中供我们使用。并在请求结束时将SecuirtyContext存进Session中便于下次使用。还有DefaultLoginPageGeneratingFilter,该过滤器在我们没有自定义配置loginPage时会自动生成,用于生成我们默认的登录页面,也就是我们一开始在搭建中看到的登录页面。对于自定义配置spring security详细参考javaDoc。spring secuirty核心过滤器以及其顺序如下(并未包括所有):

    二、认证与授权

      认证(Authentication):确定一个用户的身份的过程。授权(Authorization):判断一个用户是否有访问某个安全对象的权限。下面讨论一下spring security中最基本的认证与授权。

      首先明确一下在认证与授权中关键的三个过滤器,其他过滤器不讨论:

        1. UsernamePasswordAuthenticationFilter:该过滤器用于拦截我们表单提交的请求(默认为/login),进行用户的认证过程吧。

        2. ExceptionTranslationFilter:该过滤器主要用来捕获处理spring security抛出的异常,异常主要来源于FilterSecurityInterceptor。

        3. FilterSecurityInterceptor:该过滤器主要用来进行授权判断。

      下面根据我们访问应用的顺序并结合源码分析一下spring security的认证与授权。代码仍然是spring-security-4 (2)spring security 基于Java配置的搭建

        1.我们在浏览器中输入http://localhost:9090/ 访问应用,因为我们的路径被spring secuirty保护起来了,我们是没有权限访问的,所以我们会被引导至登录页面进行登录。

      此路径因为不是表单提交的路径(/login),该过程主要起作用的过滤器为FilterSecurityInterceptor。其部分源码如下:

    ...
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(request, response, chain);
            invoke(fi);
        }
        public void invoke(FilterInvocation fi) throws IOException, ServletException {
            //过滤器对每个请求只处理一次
            if ((fi.getRequest() != null)
                    && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                    && observeOncePerRequest) {
                // filter already applied to this request and user wants us to observe
                // once-per-request handling, so don't re-do security checking
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            else {
                // first time this request being called, so perform security checking
                if (fi.getRequest() != null) {
                    fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
                }
    
                //前处理
                InterceptorStatusToken token = super.beforeInvocation(fi);
    
                try {
                    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                }
                finally {
                    //使SecurityContextHolder中的Authentication保持原样,因为RunAsManager会暂时改变
                    //其中的Authentication
                    super.finallyInvocation(token);
                }
    
                //调用后的处理
                super.afterInvocation(token, null);
            }
        }
    ...

      真正进行权限判断的为beforeInvocation,该方法定义在FilterSecurityInterceptor的父类AbstractSecurityInterceptor中,源码如下:

    ...
        protected InterceptorStatusToken beforeInvocation(Object object) {
            Assert.notNull(object, "Object was null");
            final boolean debug = logger.isDebugEnabled();
    
            //判断object是否为过滤器支持的类型,在这里是FilterInvocation(里面记录包含了请求的request,response,FilterChain)
            //这里可以把FilterInvocation看做是安全对象,因为通过它可以获得request,通过request可以获得请求的URI。
            //而实际的安全对象就是URI
            if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
                throw new IllegalArgumentException(
                        "Security invocation attempted for object "
                                + object.getClass().getName()
                                + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                                + getSecureObjectClass());
            }
    
            
            //获取安全对象所对应的ConfigAttribute,ConfigAtrribute实际就是访问安全所应该有的权限集。
            Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                    .getAttributes(object);
    
            //判断安全对象是否拥有权限集,没有的话说明所访问的安全对象是一个公共对象,就是任何人都可以访问的。
            if (attributes == null || attributes.isEmpty()) {
                //如果rejectPublicInvocations为true,说明不支持公共对象的访问,此时会抛出异常。
                if (rejectPublicInvocations) {
                    throw new IllegalArgumentException(
                            "Secure object invocation "
                                    + object
                                    + " was denied as public invocations are not allowed via this interceptor. "
                                    + "This indicates a configuration error because the "
                                    + "rejectPublicInvocations property is set to 'true'");
                }
    
                if (debug) {
                    logger.debug("Public object - authentication not attempted");
                }
    
                publishEvent(new PublicInvocationEvent(object));
    
                return null; // no further work post-invocation
            }
    
            if (debug) {
                logger.debug("Secure object: " + object + "; Attributes: " + attributes);
            }
    
            //判断SecurityCntext中是否存在Authentication,不存在则说明访问着根本没登录
            //调用下面的credentialsNotFound()方法则会抛出一个AuthenticationException,
            //该异常会被ExceptionTranslationFilter捕获,并做出处理。
            //不过默认情况下Authentication不会为null,因为AnonymouseFilter会默认注册到
            //过滤链中,如果用户没登录的话,会将其当做匿名用户(Anonymouse User)来对待。
            //除非你自己将AnonymouseFilter从过滤链中去掉。
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                credentialsNotFound(messages.getMessage(
                        "AbstractSecurityInterceptor.authenticationNotFound",
                        "An Authentication object was not found in the SecurityContext"),
                        object, attributes);
            }
    
            //Autentication存在,则说明用户已经被认证(但是不表示已登录,因为匿名用户也是相当于被认证的),
            //判断用户是否需要再次被认证,如果你配置了每次访问必须重新验证,那么就会再次调用AuthenticationManager
            //的authenticate方法进行验证。
            Authentication authenticated = authenticateIfRequired();
    
            // Attempt authorization
            try {
                //判断用户是否有访问被保护对象的权限。
                //ed。默认的AccessDesicisonManager的实现类是AffirmativeBased
                //AffirmativeBased采取投票的形式判断用户是否有访问安全对象的权限
                //票就是配置的Role。AffirmativeBased采用WebExpressionVoter进行投票
                this.accessDecisionManager.decide(authenticated, object, attributes);
            }
            catch (AccessDeniedException accessDeniedException) {
                publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                        accessDeniedException));
    
                throw accessDeniedException;
            }
    
            if (debug) {
                logger.debug("Authorization successful");
            }
    
            if (publishAuthorizationSuccess) {
                publishEvent(new AuthorizedEvent(object, attributes, authenticated));
            }
    
            // Attempt to run as a different user
            Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                    attributes);
    
            if (runAs == null) {
                if (debug) {
                    logger.debug("RunAsManager did not change Authentication object");
                }
    
                // no further work post-invocation
                return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                        attributes, object);
            }
            else {
                if (debug) {
                    logger.debug("Switching to RunAs Authentication: " + runAs);
                }
    
                SecurityContext origCtx = SecurityContextHolder.getContext();
                SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
                SecurityContextHolder.getContext().setAuthentication(runAs);
    
                // need to revert to token.Authenticated post-invocation
                return new InterceptorStatusToken(origCtx, true, attributes, object);
            }
        }
    ...

       看这段代码,请明确几点。

        a). beforeInvocation(Object object)中的object为安全对象,类型为FilterInvocation。安全对象就是受spring security保护的对象。虽然按道理来说安全对象应该是我们访问的url,但是FilterInvocation中封装了request,那么url也可以获取到。

        b). Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object) 每个安全对象都会有对应的访问权限集(Collection<ConfigAttribute>),而且在容器启动后所有安全对象的所有权限集就已经被获取到并被放在安全元数据中(SecurityMetadataSource中),通过安全元数据可以获取到各个安全对象的权限集。因为我们每个安全对象都是登录才可以访问的(anyRequest().authenticated()),这里我们只需要知道此时每个对象的权限集只有一个元素,并且是authenticated。如果一个对象没有权限集,说明它是一个公共对象,不受spring security保护。

        c). 当我们没有登录时,我们会被当做匿名用户(Anonymouse)来看待。被当做匿名用户对待是AnonymouseAuthenticationFilter来拦截封装成一个Authentication对象,当用户被认证后就会被封装成一个Authentication对象。Authentication对象中封装了用户基本信息,该对象会在认证中做详细介绍。AnonymouseAuthenticationFilter也是默认被注册的。

        d). 最中进行授权判断的是AccessDecisionManager的子类AffirmativeBased的decide方法。我在来看其decide的源码:

    ...
    public void decide(Authentication authentication, Object object,
                Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
            int deny = 0;
    
            for (AccessDecisionVoter voter : getDecisionVoters()) {
                //根据用户的authenticton和权限集得出能否访问的结果
                int result = voter.vote(authentication, object, configAttributes);
    
                if (logger.isDebugEnabled()) {
                    logger.debug("Voter: " + voter + ", returned: " + result);
                }
    
                switch (result) {
                case AccessDecisionVoter.ACCESS_GRANTED:
                    return;
                case AccessDecisionVoter.ACCESS_DENIED:
                    deny++;
    
                    break;
    
                default:
                    break;
                }
            }
    
            if (deny > 0) {
                //如果deny>0说明没有足够的权限去访问安全对象,此时抛出的
                //AccessDeniedException会被ExceptionTranslationFilter捕获处理。
                throw new AccessDeniedException(messages.getMessage(
                        "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
            }
    
            // To get this far, every AccessDecisionVoter abstained
            checkAllowIfAllAbstainDecisions();
    }
    ...

      因为我们首次登录,所以会抛出AccessDeniedexception。此异常会被ExceptionTranslationFilter捕获并进行处理的。其部分源码如下:

    ...
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    
            try {
                chain.doFilter(request, response);
    
                logger.debug("Chain processed normally");
            }
            catch (IOException ex) {
                throw ex;
            }
            catch (Exception ex) {
                // Try to extract a SpringSecurityException from the stacktrace
                Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
                RuntimeException ase = (AuthenticationException) throwableAnalyzer
                        .getFirstThrowableOfType(AuthenticationException.class, causeChain);
    
                if (ase == null) {
                    ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                            AccessDeniedException.class, causeChain);
                }
    
                if (ase != null) {
                    //真正处理异常的地方
                    handleSpringSecurityException(request, response, chain, ase);
                }
                else {
                    // Rethrow ServletExceptions and RuntimeExceptions as-is
                    if (ex instanceof ServletException) {
                        throw (ServletException) ex;
                    }
                    else 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);
                }
            }
    }
    
    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) {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                    logger.debug(
                            "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); 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);
                }
            }
    }
    ...

      因为我们是以匿名用户的身份进行登录的,所以,会被引导去登录页面。登录页面的创建是由默认注册的过滤器DefaultLoginPageGeneratingFilter产生的。具体怎么产生的这里不做分析。我们只需要是谁做的就可以了。实际在使用时我们也不大可能去用默认生成的登录页面,因为太丑了。。。

      2.在被引导至登录页面后,我们将输入用户名和密码,提交至应用。应用会校验用户名和密码,校验成功后,我们成功访问应用。

      

    此时访问的路径为/login,这是UsernamePasswordAuthenticationFilter将拦截请求进行认证。UsernamePasswordAuthenticationFilter的doFilter方法定义在其父类AbstractAuthenticationProcessingFilter中,源码如下:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    
            //判断请求是否需要进行验证处理。默认对/login并且是POST请求的路径进行拦截
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
    
                return;
            }
    
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }
    
            Authentication authResult;
    
            try {
                //调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法进行验证,并返回
                //完整的被填充的Authentication对象
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
    
                //进行session固定攻击的处理
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch (InternalAuthenticationServiceException failed) {
                logger.error(
                        "An internal error occurred while trying to authenticate the user.",
                        failed);
                unsuccessfulAuthentication(request, response, failed);
    
                return;
            }
            catch (AuthenticationException failed) {
                // 认证失败后的处理
                unsuccessfulAuthentication(request, response, failed);
    
                return;
            }
    
            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
    
            //认证成功后的处理
            successfulAuthentication(request, response, chain, authResult);
      }

       实际认证发生在UsernamePasswordAuthenticationFilter的attemptAuthentication中,如果认证失败,则会调用unsuccessfulAuthentication进行失败后的处理,一般是提示用户认证失败,要求重新输入用户名和密码,如果认证成功,那么会调用successfulAuthentication进行成功后的处理,一般是将Authentication存进SecurityContext中并跳转至之前访问的页面或者默认页面(这部分在读者读完本节后自行去看源码是怎么处理的,这里不做讨论,现在只需知道会跳到一开始我们访问的页面中)。下面我们来看认证即attemptAuthentication的源码:

    ...
    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String username = obtainUsername(request);
            String password = obtainPassword(request);
    
            if (username == null) {
                username = "";
            }
    
            if (password == null) {
                password = "";
            }
    
            username = username.trim();
    
            //将用户名和密码封装在Authentication的实现UsernamePasswordAuthenticationToken
            //以便于AuthentictionManager进行认证
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            //获得AuthenticationManager进行认证
            return this.getAuthenticationManager().authenticate(authRequest);
    }
    ...

      spring security在进行认证时,会将用户名和密码封装成一个Authentication对象,在进行认证后,会将Authentication的权限等信息填充完全返回。Authentication会被存在SecurityContext中,供应用之后的授权等操作使用。此处介绍下Authentication,Authentication存储的就是访问应用的用户的一些信息。下面是Authentication源码:

    public interface Authentication extends Principal, Serializable {
        //用户的权限集合
        Collection<? extends GrantedAuthority> getAuthorities();
    
        //用户登录的凭证,一般指的就是密码
        Object getCredentials();
    
        //用户的一些额外的详细信息,一般不用
        Object getDetails();
    
        //这里认为Principal就为登录的用户
        Object getPrincipal();
    
        //是否已经被认证了
        boolean isAuthenticated();
    
        //设置认证的状态
        void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    }

      讲解了Authentication后,我们回过头来再看attemptAuthentication方法,该方法会调用AuthenticationManager的authenticate方法进行认证并返回一个填充完整的Authentication对象。

      在这里我们又要讲解一下认证的几个核心的类,很重要!

        a). AuthenticationManager  b).ProviderManager  c).AuthenticationProvider  d).UserDetailsService  e).UserDetails

      现在来说一下这几个类的作用以及关联关系。

        a). AuthenticationManager是一个接口,提供了authenticate方法用于认证。

        b). AuthenticationManager有一个默认的实现ProviderManager,其实现了authenticate方法。

        c). ProviderManager内部维护了一个存有AuthenticationProvider的集合,ProviderManager实现的authenticate方法再调用这些AuthenticationProvider的authenticate方法去认证,表单提交默认用的AuthenticationProvider实现是DaoAuthenticationProvider。

        d). AuthenticationProvider中维护了UserDetailsService,我们使用内存中的用户,默认的实现是InMemoryUserDetailsManager。UserDetailsService用来查询用户的详细信息,该详细信息就是UserDetails。UserDetails的默认实现是User。查询出来UserDetails后再对用户输入的密码进行校验。校验成功则将UserDetails中的信息填充进Authentication中返回。校验失败则提醒用户密码错误。

      以上说的这些接口的实现类是由我们在MySecurityConfig中配置时生成的,即下面的代码

    @Autowired
        public void configUser(AuthenticationManagerBuilder builder) throws Exception {
            builder
                .inMemoryAuthentication()
                    //创建用户名为user,密码为password的用户
                    .withUser("user").password("password").roles("USER");
        }

      这里不再讨论具体是怎么生成的,记住即可。因为我们实际在项目中一般都会用自定义的这些核心认证类。

      下面我们来分析源码,先来看ProviderManager的authenticate方法:

    ...
    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Class<? extends Authentication> toTest = authentication.getClass();
            AuthenticationException lastException = null;
            Authentication result = null;
            boolean debug = logger.isDebugEnabled();
    
            //获取所有AuthenticationProvider,循环进行认证
            for (AuthenticationProvider provider : getProviders()) {
                if (!provider.supports(toTest)) {
                    continue;
                }
    
                if (debug) {
                    logger.debug("Authentication attempt using "
                            + provider.getClass().getName());
                }
    
                try {
                    //对authentication进行认证
                    result = provider.authenticate(authentication);
    
                    if (result != null) {
                        //填充成完整的Authentication
                        copyDetails(authentication, result);
                        break;
                    }
                }
                catch (AccountStatusException e) {
                    prepareException(e, authentication);
                    // SEC-546: Avoid polling additional providers if auth failure is due to
                    // invalid account status
                    throw e;
                }
                catch (InternalAuthenticationServiceException e) {
                    prepareException(e, authentication);
                    throw e;
                }
                catch (AuthenticationException e) {
                    lastException = e;
                }
            }
    
            if (result == null && parent != null) {
                // Allow the parent to try.
                try {
                    result = parent.authenticate(authentication);
                }
                catch (ProviderNotFoundException e) {
                    // ignore as we will throw below if no other exception occurred prior to
                    // calling parent and the parent
                    // may throw ProviderNotFound even though a provider in the child already
                    // handled the request
                }
                catch (AuthenticationException e) {
                    lastException = e;
                }
            }
    
            if (result != null) {
                if (eraseCredentialsAfterAuthentication
                        && (result instanceof CredentialsContainer)) {
                    // Authentication is complete. Remove credentials and other secret data
                    // from authentication
                    ((CredentialsContainer) result).eraseCredentials();
                }
    
                eventPublisher.publishAuthenticationSuccess(result);
                return result;
            }
    
            // Parent was null, or didn't authenticate (or throw an exception).
    
            if (lastException == null) {
                //如果所有的AuthenticationProvider进行认证完result仍然为null
                //此时表示为提供AuthenticationProvider,抛出ProviderNotFoundException异常
                lastException = new ProviderNotFoundException(messages.getMessage(
                        "ProviderManager.providerNotFound",
                        new Object[] { toTest.getName() },
                        "No AuthenticationProvider found for {0}"));
            }
    
            prepareException(lastException, authentication);
    
            throw lastException;
    }
    ...

       ProviderManager用AuthenticationProvider对authentication进行认证。如果没有提供AuthenticationProvider,那么最终将抛出ProviderNotFoundException。

      我们表单提交认证时,AuthenticationProvider默认的实现是DaoAuthenticationProvider,DaoAuthenticationProvider的authenticate方法定义在其父类AbstractUserDetailsAuthenticationProvider中,其源码如下:

    ...
    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));
    
            // Determine username
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                    : authentication.getName();
    
            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);
    
            if (user == null) {
                cacheWasUsed = false;
    
                try {
                    //获取UserDetails,即用户详细信息
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                catch (UsernameNotFoundException notFound) {
                    logger.debug("User '" + username + "' not found");
    
                    if (hideUserNotFoundExceptions) {
                        throw new BadCredentialsException(messages.getMessage(
                                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                                "Bad credentials"));
                    }
                    else {
                        throw notFound;
                    }
                }
    
                Assert.notNull(user,
                        "retrieveUser returned null - a violation of the interface contract");
            }
    
            try {
                preAuthenticationChecks.check(user);
                //进行密码校验
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    // There was a problem, so try again after checking
                    // we're using latest data (i.e. not from the cache)
                    cacheWasUsed = false;
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                    preAuthenticationChecks.check(user);
                    additionalAuthenticationChecks(user,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                else {
                    //认证失败抛出认证异常
                    throw exception;
                }
            }
    
            postAuthenticationChecks.check(user);
    
            if (!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }
    
            Object principalToReturn = user;
    
            if (forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }
    
            //认证成功,返回装有用户权限等信息的authentication对象
            return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    ...

      retrieveUser方法定义在DaoAuthenticationProvider中,用来获取UserDetails这里不再展示源码,请读者自行去看。你会发现获取获取UserDetails正是由其中维护的UserDetailsService来完成的。获取到UserDetails后再调用其

      additionalAuthenticationChecks方法进行密码的验证。如果认证失败,则抛出AuthenticationException,如果认证成功则返回装有权限等信息的Authentication对象。

    三、总结

      到目前为止,我们结合我们创建的项目和spring security的源码分析了web应用认证和授权的原理。内容比较多,现在理一下重点。

      1.springSecurityFilterChain中各个过滤器怎么创建的只需了解即可。不要太过关注。

      2.重点记忆UsernamePasswordAuthenticationFilter,ExceptionTranslationFilter,FilterSecurityInterceptor这三个过滤器的作用及源码分析。

      3.重要记忆认证中Authentication,AuthenticationManager,ProviderManager,AuthenticationProvider,UserDetailsService,UserDetails这些类的作用及源码分析。

      4.重点记忆授权中FilterInvoction,SecurityMetadataSource,AccessDecisionManager的作用。

      5.将这些类理解的关键是建立起关联,建立起关联的方式就是跟着本节中的案例走下去,一步步看代码如何实现的。

    参考资料:http://www.tianshouzhi.com/api/tutorials/spring_security_4/250

         https://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/reference/htmlsingle/

  • 相关阅读:
    Kafka 入门(四)-- Python Kafka Client 性能测试
    XShell连接阿里云服务器出现”用户密钥加载失败:请确定输入的密码“处理办法
    优化自动化测试流程,使用 flask 开发一个 toy jenkins工具
    我做了回视频,告诉你需要用到哪些工具
    提问的基本原则
    12 月31 日返利系统问题复盘
    外部prometheus监控k8s(k3s)集群
    一个Java类在运行时候,变量是怎么在JVM中分布的呢?
    JVM学习第二篇思考:一个Java代码是怎么运行起来的-下篇
    JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇
  • 原文地址:https://www.cnblogs.com/wutianqi/p/9186645.html
Copyright © 2020-2023  润新知