认证篇:Spring Security 表单登录认证源码解析

图1-0 表单登录源码解析总结图

原理讲解

接上回分解,上回我们聊到了认证(一):基于表单登录的认证模式,正所谓:“知其然,然后知其所以然”;就让我们来一探究竟,瞅瞅 Spring Security到底是怎么实现表单登录的。

首先我们先来回顾一下上篇文章我们制作的 Spring Security的表单认证的流程图:

图1-1 Spring Security表单登录认证流程图

从流程图中我们可以简单的了解到,整个认证流程大致上分为3个模块:

  • 登录信息的封装

  • 认证

  • 收尾处理(成功&失败处理)

核心模块为 认证模块,下面就来看看认证模块 AuthenticationManager的相关类图:

图1-2 认证类图

该图可以分2块来看,分别是左边负责掌控全局的"大哥",以及右边勤勤恳恳的"小弟们"。

总所周知,“大哥"都不需要亲历亲为的,所以"大哥” AuthenticationManager认证管理接口,只定义了认证方法 authenticate(),具体咋实现就让小弟们去搞吧~~

“二当家” ProviderManager为认证管理类,实现了 AuthenticationManager(二当家肯定要听大哥的话),并在认证方法 authenticate()中将身份认证委托给具有认证资格的 AuthenticationProvider(真正干活的小弟们);同时
ProviderManaer有一个成员变量 List<AuthenticationProvider> providers用以存储了所有具体执行认证的"小弟们"。

接下来介绍一下右边勤勤恳恳的"小弟们",首先是AuthenticationProvider认证接口类,其定义了身份认证方法authenticate();这个也比较好理解;你怎么证明自己是我的"小弟"呢?当然是得入我门为我干活拉!AuthenticationProvider接口就是起这个作用。

AbstractUserDetailAuthenticationProvider为认证抽象类,实现了接口AuthenticationProvider,同时还定义了抽象方法retrieveUser()用于从数据库中获取用户信息,以及additionalAuthenticationChecks()做身份认证;这块可能会犯迷糊,为啥子这个"小弟"还是个抽象类呢?不必慌张,其实只是为了一些功能的复用。

DaoAuthenticationProvider认证类继承于AbstractUserDetailAuthenticationProvider抽象认证类,实现了上面提到的2个抽象方法retrieveUser和additionalAuthenticationChecks;并自定义了一些成员变量:private UserDetailsService userDetailsService;用以用户信息查询,以及private PasswordEncoder passwordEncoder用作密码的加密认证。

源码解析

在大致了解了原理之后,就开始了我们的阅读源码之旅拉;分两个模块来看,分别是:登录信息的封装以及 认证

登录信息的封装

登录信息的封装是指将前端传递的username和password封装成 UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationFilter.class的 attemptAuthentication()方法

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);
        String password = this.obtainPassword(request);
        if (username == null) {
    
    
            username = "";
        }

        if (password == null) {
    
    
            password = "";
        }

        username = username.trim();
        // 将http请求的Request带的认证参数:username、password转换为认证的token对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 设置一些详细信息, 诸如发送请求的ip等...
        this.setDetails(request, authRequest);
        // 调用AuthenticationManager的authenticate方法 执行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

attemptAuthentication()方法做的事情很简单,主要是将登录信息 username和password封装成 UsernamePasswordAuthenticationToken。那么这个Token到底是起什么作用呢?其实也很简单,主要是用于后续认证的时候,寻找匹配的认证处理器,例如表单登录的 UsernamePasswordAuthenticationToken会唯一匹配相应的认证Provider

认证

从上面我们也可以看到,在将登录信息封装成Token后,就调用了 AuthenticationManagerauthenticate()方法执行认证操作;因 AuthenticationManager是一个接口,我们来分析它的实现类 ProviderManager

ProviderManager.class的authenticate()方法

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();

    // 获取所有干活的“小弟” providers 认证器
    Iterator var6 = this.getProviders().iterator();

    // 挨个遍历,找到能支持当前登录方式(表单登录---由token来区分)的认证器
    while(var6.hasNext()) {
    
    
        AuthenticationProvider provider = (AuthenticationProvider)var6.next();
        // 之前我们介绍过 AuthenticationProvider 接口,里面定义的supports方法,就是用于判定一个provider支持那种类型的认证方式
        if (provider.supports(toTest)) {
    
    
            if (debug) {
    
    
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            }

            // 匹配到对应的provider后,调用provider的authenticate方法进行认证
            try {
    
    
                result = provider.authenticate(authentication);
                if (result != null) {
    
    
                    // 认证成功,copy一些细节的参数到认证对象上
                    this.copyDetails(authentication, result);
                    break;
                }
            } catch (AccountStatusException var11) {
    
    
                this.prepareException(var11, authentication);
                throw var11;
            } catch (InternalAuthenticationServiceException var12) {
    
    
                this.prepareException(var12, authentication);
                throw var12;
            } catch (AuthenticationException var13) {
    
    
                lastException = var13;
            }
        }
    }

    if (result == null && this.parent != null) {
    
    
        try {
    
    
            result = this.parent.authenticate(authentication);
        } catch (ProviderNotFoundException var9) {
    
    
        } catch (AuthenticationException var10) {
    
    
            lastException = var10;
        }
    }

    if (result != null) {
    
    
        if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
    
    
            ((CredentialsContainer)result).eraseCredentials();
        }

        this.eventPublisher.publishAuthenticationSuccess(result);
        return result;
    } else {
    
    
        if (lastException == null) {
    
    
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{
    
    toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }

        this.prepareException((AuthenticationException)lastException, authentication);
        throw lastException;
    }
}

ProviderManagerauthenticate()方法阅读起来也不困难,目的性十分的明确;首先是找到所有的认证器(干活的“小弟们”),挨个遍历根据Token进行匹配,如果匹配成功则进行认证。因本文分析的是表单登录,所以根据UsernamePasswordAuthenticationToken匹配到的 ProviderDaoAuthenticationProvider

DaoAuthenticationProvider.class的 authenticate()方法 (PS: DaoAuthenticationProvider继承于抽象类 AbstractUserDetailsAuthenticationProvider,自身并无authenticate())

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
    // 前置检查 该provider只支持 UsernamePasswordAuthenticationToken的认证方式
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
        this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));

    String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    // 尝试从缓存中获取用户信息
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
    
    
        cacheWasUsed = false;

        // 从缓存中获取不到用户信息, 调用子类 DaoAuthenticationProvider的retrieveUser方法,从数据库中加载用户信息
        try {
    
    
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
    
    
            this.logger.debug("User '" + username + "' not found");
            if (this.hideUserNotFoundExceptions) {
    
    
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }

            throw var6;
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
    
    
        // 预检查,之前我们介绍UserDetails的时候,有提到过几个方法,例如判断账号是否可用、账号是否过期等...
        this.preAuthenticationChecks.check(user);
        // 认证操作, 调用子类DaoAuthenticationProvider实现的additionalAuthenticationChecks进行认证
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
    
    
        if (!cacheWasUsed) {
    
    
            throw var7;
        }

        cacheWasUsed = false;
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
    
    
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
    
    
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

阅读代码我们可以看出,首先先尝试用缓存中获取用户,当从缓存中获取不到用户的时候,调用子类DaoAuthenticationProvider实现的 retrieveUser()方法,从数据库中加载用户信息,具体代码如下:

DaoAuthenticationProvider.class的 reretrieveUser()方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    
    // 检查passwordEncoder
    this.prepareTimingAttackProtection();

    try {
    
    
        // UserDetailsService的loadUserByUsername方法,根据用户名从数据库中获取用户信息,是不是很熟悉~~~
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
    
    
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
    
    
            return loadedUser;
        }
    } catch (UsernameNotFoundException var4) {
    
    
        this.mitigateAgainstTimingAttack(authentication);
        throw var4;
    } catch (InternalAuthenticationServiceException var5) {
    
    
        throw var5;
    } catch (Exception var6) {
    
    
        throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    }
}

private void prepareTimingAttackProtection() {
    
    
    if (this.userNotFoundEncodedPassword == null) {
    
    
        this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
    }

}

当加载完用户信息,进行预检查后,就调用子类DaoAuthenticationProvider.class的additionalAuthenticationChecks()进行最终的认证校验

DaoAuthenticationProvider.class的additionalAuthenticationChecks()方法

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    
    // 认证请求的密码非空判断
    if (authentication.getCredentials() == null) {
    
    
        this.logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
    
    
        // 调用passwordEncoder的matches匹配方法,判断前端传递的密码和从数据库load出来的密码是否匹配
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    
    
            this.logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

总结

总体来说,从登录信息的封装到最终的认证都比较的连贯;既然我们已经对Spring Security的认证体系有了一定的了解,接下来我们也来尝试定制化开发自己的认证方式吧!

文章到这就结束拉,欢迎大家扫码关注小奇公众号~
请添加图片描述

猜你喜欢

转载自blog.csdn.net/weixin_46920376/article/details/108810153