添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
气宇轩昂的便当  ·  China's Duku Highway ...·  1 年前    · 
任性的大脸猫  ·  棋城新聞·  1 年前    · 
满身肌肉的滑板  ·  遠東百貨·  1 年前    · 

之前看过很多个版本的 Spring Security 中获取用户信息并进行密码校验,有在相关的Filter中获取的,也有在默认的 DaoAuthenticationProvider 中判断 Authentication 类型进行判断以后直接获取的。

不过,这些方式都不太“正宗”, Spring Security 有自己的一套流程。至于此流程的详细信息讲解,先不急,本篇文章来认识一下其中比较重要的一环:获取用户信息并进行密码验证,即 DaoAuthenticationProvider 。

AuthenticationProvider

先来认识一下其实现的接口 AuthenticationProvider ,主要用于解析特定 Authentication 。

* Indicates a class can process a specific * {@link org.springframework.security.core.Authentication} implementation.

其中,有两个接口方法。

首先是 authenticate(Authentication authentication) 方法。

Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

该方法与 AuthenticationManager 中的 authenticate 声明及功能完全一致,返回包含凭据的完整身份验证对象 authentication 。但是,如果 AuthenticationProvider 不支持给定的 Authentication 的话,该方法可能会返回 null 。在此情况下,下一个支持 authentication 的 AuthenticationProvider 将会被尝试(关于此段逻辑, AuthenticationManager 中会有详细的逻辑代码及说明,后续再详细讲解)。

其次是 supports(Class<?> authentication) 方法。

boolean supports(Class<?> authentication);

如果 AuthenticationProvider 支持给定的 Authentication 的话,会返回 true 。但是,这并不保证 AuthenticationProvider 能够对给定的 Authentication 进行身份认证,它只是表明它可以支持对其进行更深入的评估, AuthenticationProvider 依然可以返回 null ,以指示应尝试另一个 AuthenticationProvider 。

此方法是用以选择一个能够匹配 Authentication 以胜任身份认证工作的 AuthenticationProvider ,交给 ProviderManager 来执行。

AbstractUserDetailsAuthenticationProvider

用于解析 UsernamePasswordAuthenticationToken 以进行身份认证的基础 AuthenticationProvider 。其子类可以重写或使用其 UserDetails 对象。

其实现了 AuthenticationProvider 接口的 authenticate(Authentication authentication) 方法,其认证过程可分为一下几个步骤。

获取用户名。

String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

缓存中获取 User。

boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

如果缓存中获取的 User 为 null,再调用 retrieveUser (子类实现)方法获取。

if (user == null) {
    cacheWasUsed = false;
    try {
        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;
    ......

如果找不到用户,会抛出 UsernameNotFoundException 异常。此时,处理方案有2个。

  • 如果 hideUserNotFoundExceptions 为 true(默认为 true),即隐藏用户未找到异常,则会重新抛出凭据/密码错误异常,异常信息为 Spring Security 框架已定义好的提示信息。

  • 如果不隐藏用户未找到异常,则直接抛出 UsernameNotFoundException 异常。

前置身份认证检查。

preAuthenticationChecks.check(user);

默认的前置身份认证检查逻辑如下,即事先校验一下用户的账号是否锁定、是否可用、是否过期。

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        if (!user.isAccountNonLocked()) {
            logger.debug("User account is locked");
            throw new LockedException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.locked",
                "User account is locked"));
        if (!user.isEnabled()) {
            logger.debug("User account is disabled");
            throw new DisabledException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.disabled",
                "User is disabled"));
        if (!user.isAccountNonExpired()) {
            logger.debug("User account is expired");
            throw new AccountExpiredException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.expired",
                "User account has expired"));

额外身份认证校验,对于 UsernamePasswordAuthenticationToken 来说就是凭据/密码校验。

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;

如果认证过程中发生异常,会有如下处理逻辑:

  1. 如果当前的用户是从用户缓存中取出的,则使用原有的用户信息再进行一次身份认证,即获取用户信息、前置身份认证检查、额外身份认证检查;

  2. 如果不是从用户缓存中取出的,则直接抛出异常。

具体的校验逻辑在下面的 DaoAuthenticationProvider 中会进行详细说明,此处暂且不谈。

后置身份认证检查。

postAuthenticationChecks.check(user);

默认的后置身份认证检查逻辑如下,即检查一下用户的凭据/密码是否过期。

private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        if (!user.isCredentialsNonExpired()) {
            logger.debug("User account credentials have expired");
            throw new CredentialsExpiredException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                "User credentials have expired"));

将当前用户放入用户缓存,如果当前用户还没有被用户缓存缓存的话。

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

转换 principal 为字符串类型。不过,需要 forcePrincipalAsString 参数为 true(默认为 false)。

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

最后,创建身份认证成功的 Authentication。

return createSuccessAuthentication(principalToReturn, authentication, user);

默认的创建身份认证成功的 Authentication 逻辑如下。

return createSuccessAuthentication(principalToReturn, authentication, user);

即创建了一个新的 UsernamePasswordAuthenticationToken,与未认证的区别就是 principal 变成了检索到的用户详细信息(或者用户名,强制字符串principal)。

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

AbstractUserDetailsAuthenticationProvider 的主要功能就是这些,剩下的,就是诸如 retrieveUser 、additionalAuthenticationChecks 等需要子类实现的抽象方法了,这就是属于子类 DaoAuthenticationProvider 的部分了。

DaoAuthenticationProvider

说到 Authentication 相信都不陌生。最著名的,便是 UsernamePasswordAuthenticationToken。而 DaoAuthenticationProvider 便是用于解析并认证 UsernamePasswordAuthenticationToken 的这样一个认证服务提供者。

类注释说的也很明白。

* An {@link AuthenticationProvider} implementation that retrieves user details from a * {@link UserDetailsService}.

其最终目的,就是根据 UsernamePasswordAuthenticationToken,获取到 username,然后调用 UserDetailsService 检索用户详细信息。

在其基类 AbstractUserDetailsAuthenticationProvider 中,我们已经讲过,需要子类实现 retrieveUser 、additionalAuthenticationChecks 等抽象方法。那么,我们就来详细看一下吧。

额外的身份认证检查,也即 additionalAuthenticationChecks,密码检查。

protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
    String presentedPassword = authentication.getCredentials().toString();
    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");
        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));

这里的检查逻辑比较简单,就不再赘述了。

接下来就是用户检索,即 retrieveUser。

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        return loadedUser;
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);

正如其类注释所述,需要调用 UserDetailsService 检索用户详细信息,如权限列表、存储密码等,逻辑也比较简单。但是,有两个特殊方法需要注意一下,即检索用户前调用的 prepareTimingAttackProtection() 方法,和抛出 UsernameNotFoundException 异常后调用的 mitigateAgainstTimingAttack(authentication) 方法。

这两个方法都是用于定时攻击保护的。

首先是 prepareTimingAttackProtection() 方法。

private void prepareTimingAttackProtection() {
    if (this.userNotFoundEncodedPassword == null) {
        this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);

该方法从方法命名上理解,是为了准备定时攻击保护用的。其实,就是将 userNotFoundEncodedPassword,也即用户未找到时的加密密码给准备好,然后使用配置的 passwordEncoder 来加密为密文,默认的用户未找到时的明文密码为 userNotFoundPassword。

private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

其次是 mitigateAgainstTimingAttack(authentication) 方法。

private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
    if (authentication.getCredentials() != null) {
        String presentedPassword = authentication.getCredentials().toString();
        this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);

逻辑也不复杂,就是在用户提供的凭据/密码不为空时,使用配置的 passwordEncoder 来验证两者是否相等。看到这里也许会有疑惑,这两者相等的概率太小了吧?一个是用户随便填的,一个是系统配置的,且无论相同与否,都不影响下面 UsernameNotFoundException 异常的抛出,除非我们在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常。

这里我们看一下方法命名中的 mitigate,意味缓解、减轻、缓和。

什么意思呢?我们需要从整体上想象一下。

用户登录时,调用 AuthenticationProvider 的 authenticate 方法,然后,先从用户缓存中获取用户,如果取不到,再调用 retrieveUser 方法检索用户。最后,再调用 additionalAuthenticationChecks 方法进行密码检查。既然定性为有目的的定时攻击,那么在发生此种情况后,就没有必要继续验证密码了,进一步也没有必要进行后续的身份认证处理了。

至于是否在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常,还是存疑的,感觉实在没有必要,这里倾向于直接返回密码检查结果,至于 UsernameNotFoundException 异常,在基类调用retrieveUser 方法是已有相关异常捕获逻辑,且可以自行配置是否隐藏该异常类型,所以,感觉没有必要。

这里也确实猜不透设计团队的意图,存个疑吧。有了解此番意图的,可联系作者说明,不胜感激!

另外,DaoAuthenticationProvider 还重写了基类的 createSuccessAuthentication 方法。

@Override
protected Authentication createSuccessAuthentication(Object principal,
                                                     Authentication authentication, UserDetails user) {
    boolean upgradeEncoding = this.userDetailsPasswordService != null
        && this.passwordEncoder.upgradeEncoding(user.getPassword());
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    return super.createSuccessAuthentication(principal, authentication, user);

还记得我们在介绍基类的 createSuccessAuthentication 方法时所说的逻辑吗?

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

该方法便是对此段逻辑描述最好的呈现了。

至于 userDetailsPasswordService,其实也很简单,就是更新一下 User 中的密码,仅此而已。

public UserDetails updatePassword(UserDetails user, String newPassword) {
    String username = user.getUsername();
    MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
    mutableUser.setPassword(newPassword);
    return mutableUser;

自定义Provider

还记得前一篇文章中自定义的适用于CA登录认证的 CertificateAuthorityAuthenticationToken 和 CertificateAuthorityAuthenticationFilter 吗?

相对应的,这里我们就自定义个适用于CA登录认证的 CertificateAuthorityAuthenticationProvider。

public class CertificateAuthorityDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private UserDetailsService userDetailsService;
    public CertificateAuthorityDaoAuthenticationProvider() {
    @Override
    public boolean supports(Class<?> authentication) {
        return (CertificateAuthorityAuthenticationToken.class.isAssignableFrom(authentication));
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        // do nothing.
  ......

由于CA登录时,其CA KEY的密码是有前端配合控制器校验的,况且其密码也不在业务数据库中存储,不需要Spring Security框架处理,所以,无需进行额外的身份认证检查。

另外,需要修改一下 supports 方法,仅支持 CertificateAuthorityAuthenticationToken 类型的 Authentication 哦。

其它详细源码,请参考文末源码链接,可自行下载后阅读。

github

https://github.com/liuminglei/SpringSecurityLearning/tree/master/26

gitee

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/26
 

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring
文章目录DaoAuthenticationProviderDeclaredJdocretrieveUserDeclaredMethod CodeadditionalAuthenticationChecksDeclaredMethod CodecreateSuccessAuthenticationDeclaredMethod Code DaoAuthenticationProvider Declar...
protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(new LoginAuthenticationProvider(loginService));
`Spring Security`作为一个功能完善的安全框架,具有以下特性: - **认证(Authentication)**:解决 "你是谁" 的问题,验证系统中是否有这个“用户”(用户/设备/系统),也就是我们常说的“登录”。 - **授权(Authorization)**:权限控制/鉴别,解决的是系统中某个用户能够访问哪些资源,即“你能干什么”的问题。`Spring Security` 支持基于 `URL` 的请求授权、方法访问授权、对象访问授权。
正文有三种情况我们需要配置多个AuthenticationProvider,甚至是自定义AuthenticationProvider: 1. 用户认证信息存储在多个地方 2. 在同一个地方但是需要多种授权方式,比如说手机号+密码可以登录,邮箱+密码也可以登录 3. 以上两种情况都有 配置AuthenticationProvider,可以通过AuthenticationManagerBuild
AbstractUserDetailsAuthenticationProvider类位于org.springframework.security.authentication.dao包下,在 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter中被调用, 该类的public Au...
开发遇到一件很尴尬的事情,springboot里面使用security做登录验证,但是默认的就是验证username和password,现在的第三方或者短信登录非常流行。我现在的需求是做账号+短信登录(ps:后面还不知道要加啥… so:旧的不能废,只能扩展),接rongcloud sdk post:/login params:{ username:mobile, sessionid:rongclo...