SpringSecurity认证流程分析

重要组件

  • SecurityContext 上下文对象,Authentication(认证)对象会放在里面
  • SecurityContextHolder 用于拿到上下文对象的静态工具类
  • Authentication 认证接口,定义了认证对象的数据形式(数据格式)
  • AuthenticationManager 用于校验Authentication,返回一个认证后的Authentication对象

SecurityContext对象负责存放认证后的对象数据

public interface SecurityContext extends Serializable {
 // 获取Authentication对象
 Authentication getAuthentication();

 // 放入Authentication对象
 void setAuthentication(Authentication authentication);
}

主要的作用就是get and set

SecurityContextHolder 用于获取上下文对象的静态工具类。用于存储上下文对象的信息。包括当前的用户是谁,是否认证、角色权限.... 都被保存在SecurityContextHolder

public class SecurityContextHolder {

    //清除上下文数据
 public static void clearContext() {
  strategy.clearContext();
 }

    //获取上下文数据
 public static SecurityContext getContext() {
  return strategy.getContext();
 }

    //放入上下文数据
 public static void setContext(SecurityContext context) {
  strategy.setContext(context);
 }
}

主要的作用就是获取和放入SecurityContext

Authentication认证接口,定义了认证对象的数据形式

public interface Authentication extends Principal, Serializable {
    //获取用户权限
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取证明用户的信息(密码等...)
 	Object getCredentials();
    //用户额外信息(用户表信息)
 	Object getDetails();
    //获取用户身份信息,未认证获取的是用户名,认证后获取的是UserDetails
    Object getPrincipal();
    //获取当前Authentication是否已认证
    boolean isAuthenticated();
    //设置当前Authentication是否已认证(true or false)
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication只是定义了一种在SpringSecurity进行认证的数据的数据形式应该是怎么样的。

AuthenticationManager【接口】是认证相关的核心接口,它定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,它的默认实现类是ProviderManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

ProviderManagerAuthenticationManager 的默认实现类,其实很大一部分工具类都是围绕着这个 ProviderManager 实现类来的,由他衍生出来的 AuthenticationProvider 接口

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    // 这里维护着一个 AuthenticationProvider 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
    // 也就是说,核心的认证入口始终只有一个:AuthenticationManager
    // 例如下面不同的认证方式,对应了三个 AuthenticationProvider:
    // 1、用户名 + 密码(UsernamePasswordAuthenticationToken),
    // 2、邮箱 + 密码,
    // 3、手机号码 + 密码登录
    // 在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。
    private List<AuthenticationProvider> providers = Collections.emptyList();


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

        // ProviderManager 中的 List(providers),会依照次序去认证,认证成功则立即返回,
        // 若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有
        // 认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
        for (AuthenticationProvider provider : getProviders()) {

            // 这个 supports 方法用来判断此AuthenticationProvider 是否支持当前的 Authentication 对象
            if (!provider.supports(toTest)) {
                continue;
            }
            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
          ...
          catch (AuthenticationException e) {
                lastException = e;
            }
        }

        // 如果有 Authentication 信息,则直接返回
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // 移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
            // 发布登录成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }
       ...

        // 执行到此,说明没有认证成功,包装异常信息
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
        prepareException(lastException, authentication);
        throw lastException;
    }
}

AuthenticationProvider接口内部有两个实现类

public interface AuthenticationProvider {
    //负责认证
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
    
    //负责判断此AuthenticationProvider是否支持当前的 Authentication 对象。
    boolean supports(Class<?> authentication);
}

就是输入一个凭证,输出一个Principal,输入输出都是封装在Authenticaiton里面的。

输入(credentials) ---->	
							AuthenticationProvider.authenticate()
输出(principal)<-------

而且可以有多种类型的认证方式。

​ 那这些 AuthenticationProvider 需要做什么工作呢?首先通过username取得数据库里面的用户数据,所以这时就可以使用这个UserDetailService.loadUserByUserName()来取得数据。这个UserDetailService会将用户数据填充到Authentication里面去【Authentication.getCredentials()】。

​ 最后的最后,为了让其他的过滤器也能够获取到这个认证的AuthenticationSpringSecurity会将其存储到上下文对象中。

DaoAuthenticationProvider就是AuthenticationProvider的实现类。

Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路,用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。

​ 在SpringSecurity中。提交的用户名和密码被封装成了UsernamePasswordAuthenticationToken,而根据用户名价值用户的认为则是提交给了UserDetailsService

如何取得用户?

​ 在DaoAuthenticationProvider中,对应的根据用户名加载用户的方法便是retrieveUser

// 虽然有两个参数,但是 retrieveUser 只有第一个参数(username)起主要作用,返回一个 UserDetails。
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);
    }
}
对比密码

​ 还需要完成UsernamePasswordAuthenticationTokenUserDetails密码的比对,这便是交给 additionalAuthenticationChecks方法完成的。

	这个方法是在父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中被调用的,如果这个 void 方法没有抛异常,则认为比对成功。
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();
    //比对密码的过程,用到了 PasswordEncoder 和 SaltSource
    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"));
    }
}

​ 总之就是 DaoAuthenticationProvider它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

UserDetails它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。

public interface UserDetails extends Serializable {
    //访问权限信息
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取密码
    String getPassword();
    //获取用户名
    String getUsername();
    //判断是没过期
    boolean isAccountNonExpired();
    //是否没有被锁定
    boolean isAccountNonLocked();
    //是否没有超时
    boolean isCredentialsNonExpired();
    //用户是否可用
    boolean isEnabled();
}

​ Authentication 的 getCredentials()UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对

​ Authentication 中的 getAuthorities() 实际是由 UserDetailsgetAuthorities() 传递而形成的。

UserDetailsService

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 它纯粹是一个用于用户数据的 DAO,除了向框架内的其他组件提供该数据之外,没有其他功能。常见的类型有,从数据库加载,从内存中加载。

总体认证流程:
  1. 首先是一个请求带着身份信息进来,用户名和密码被过滤器获取到,封装成Authentication默认情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 这个Authentication经过AuthenticationManager的认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的Authentication实例。
  3. SecurityContextHolder安全上下文容器将上面充满信息的Authentication通过SecurityContextHolder.getContext().setAuthentication(...)方法设置到SecurityContext上下文对象中。
  4. Authentication 对象中拿到我们的 UserDetails 对象,之前我们说过,认证后的 Authentication 对象调用它的 getPrincipal() 方法就可以拿到我们先前数据库查询后组装出来的 UserDetails 对象,然后创建 token。
  5. UserDetails 对象放入缓存中,方便后面过滤器使用。

这样的话就算完成了,,因为主要认证操作都会由 AuthenticationManager.authenticate() 帮我们完成。

authenticate方法

这个 authenticate 方法就是认证的核心方法,它位于 AbstractUserDetailsAuthenticationProvider 这个抽象类里面,而大部分具体的 AuthenticationProvider 都是先继承自这个抽象类。

// AbstractUserDetailsAuthenticationProvider
// 注意这个 AbstractUserDetailsAuthenticationProvider 类是上面的 DaoAuthenticationProvider 的父类,
// 认证部分都是在这个父类做的
public Authentication authenticate(Authentication authentication) {

    // 校验未认证的Authentication对象里面有没有用户名
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    boolean cacheWasUsed = true;
    // 从缓存中去查用户名为XXX的对象
    UserDetails user = this.userCache.getUserFromCache(username);
    // 如果没有就进入到这个方法
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 调用我们重写 UserDetailsService 的 loadUserByUsername 方法
            // 拿到我们自己组装好的 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);
    }
}
逻辑模型
public static void main(String[] args) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		AuthenticationManager am = new SampleAuthenticationManager();
        while(true) {
            System.out.println("请输入您的用户名:");
            String name = in.readLine();
            System.out.println("请输入您的密码:");
            String password = in.readLine();

            try {
                // 1、封装一个 UsernamePasswordAuthenticationToken 对象
                Authentication request = new UsernamePasswordAuthenticationToken(name, password);

                // 2、经过 AuthenticationManager 的认证,如果认证失败会抛出一个 AuthenticationException 错误
                Authentication result = am.authenticate(request);

                // 3、将这个认证过的 Authentication 填入 SecurityContext 里面
                SecurityContextHolder.getContext().setAuthentication(result);
                break;
            } catch(AuthenticationException e) {
                System.out.println("认证失败:" + e.getMessage());
            }
        }

        System.out.println("认证成功: Security context contains:\n" +
                SecurityContextHolder.getContext().getAuthentication());
    }

	// 关键认证部分
    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {

        // getCredentials 返回的是密码,这里随便写了,直接用户名和密码一致就算登陆成功
        if (auth.getName().equals(auth.getCredentials())) {
            // 认证成功返回一个已经认证的 UsernamePasswordAuthenticationToken 的对象,并把这个用户的权限填入
            return new UsernamePasswordAuthenticationToken(auth.getName(),
                    auth.getCredentials(), AUTHORITIES);
        }
        throw new BadCredentialsException("Bad Credentials");
    }
posted @ 2022-08-08 23:29  MineLSG  阅读(172)  评论(0编辑  收藏  举报