《Spring Security》第七章 Spring Security 中的核心服务

第七章 Spring Security 中的核心服务

SpringSecurity中的核心组件与服务

一、核心接口与其实现

第五章 SpringSecurity 中的核心组件第六章 SpringSecurity 身份认证流程 中介绍了 Spring Security 体系结构及其核心类。下面更加深入的了解一些核心接口与其实现方法。

上图描述了一个请求认证,大致需要经过的过滤器、接口,及各个接口的子类实现。

其中标黄的部分是下面讲解的重点。

具体的接口与实现类的含义,可以参考官方网站中的解释并结合图中的关系进行梳理。我只会挑选一些核心的实现方法来说。

如果不想看源码,请直接看我写的注释,也能明白这些类的执行过程。

1. UsernamePasswordAuthenticationFilter

拦截并处理用户的登录请求

public Authentication attemptAuthentication(HttpServletRequest request,
                                            HttpServletResponse response) throws AuthenticationException {
    // 1. 登录认证请求必须是POST
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }

    // 2. 检查请求 request 中是否包含 username、password 参数
    String username = obtainUsername(request);
    String password = obtainPassword(request);

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

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

    username = username.trim();

    // 3. 将 username、password 封装为一个 UsernamePasswordAuthenticationToken 对象
    // 该对象是 Authentication 的实现类
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);

    setDetails(request, authRequest);

    // 4. 调用 AuthenticationManager 的认证方法
    return this.getAuthenticationManager().authenticate(authRequest);
}

2. ProviderManager

上面调用 authenticate(authRequset) 的 AuthenticationManager 是一个接口类,其具体实现是 ProviderManager。当然,也可以选择其他实现类或者自定义。

/**
 * 该方法用于验证 authentication(也就是上面传入的 UsernamePasswordAuthenticationToken)
 */
public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();

    // 1. 循环遍历 AuthenticationProvider 的实现类集合
    // 如果有超过一个的 provider 支持,则使用第一个
    // 如果一个都不支持,则返回 AuthenticationException 异常
    for (AuthenticationProvider provider : getProviders()) {
        // 2. 判断是否支持
        if (!provider.supports(toTest)) {
            continue;
        }

        if (debug) {
            logger.debug("Authentication attempt using "
                         + provider.getClass().getName());
        }

        try {
            // 3. 调用 provider 的认证,下面会以 DaoAuthenticationProvider 为例进行介绍
            result = provider.authenticate(authentication);

            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException e) {
            prepareException(e, authentication);
            throw e;
        }
        catch (InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            throw e;
        }
        catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result == null && parent != null) {
        try {
            // 4. 进行父类的认证尝试
            result = parent.authenticate(authentication);
        }
        catch (ProviderNotFoundException e) {

        }
        catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result != null) {
        if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
            // 5. 认证完成,请求结束之后,删除 SecurityContextHolder 中保存的 SecurityContext,清除相关数据
            ((CredentialsContainer) result).eraseCredentials();
        }

        // 6. 发送验证成功的事件
        eventPublisher.publishAuthenticationSuccess(result);
        return result;
    }

    // 7. 身份验证失败,抛出异常
    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
    }

    prepareException(lastException, authentication);

    throw lastException;
}

3. DaoAuthenticationProvider

DaoAuthenticationProvider 包含两个接口属性 PasswordEncoder 与 UserDetailsService,通过 UserDetailsService 加载用户数据,并使用 PasswordEncoder 比较 UserDetails.password 与 token.password 是否相等。

以此来决定是否认证成功。

protected final UserDetails retrieveUser(String username,
                                         UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        // 调用 UserDetailsService 的 loadUserByUsername 方法,查询装载 UserDetails
        // 在实际开发中,通常会自定义 UserDetailsService 的实现,例如将数据库查询的 User 对象与 UserDetails 对象进行一个适配
        // 适配者也需要我们自己定义,如 CustomUserDetails 实现 UserDetails,并将 user.role 转换为 authority等。
        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);
    }
}

4. UserDetailsService

UserDetailsService 是整个 Dao 认证框架的核心,用来装载特殊的用户数据。

因为该类为接口类,只声明了一个 loadUserByUsername(String) 方法,所以这里我们使用自定义实现来演示在日常开发场景中的使用过程:

// 1. 实现 UserDetailsService 接口,自定义获取用户的方法
@Service
public class DemoUserDetailsService implements UserDetailsService {

    // 2. 注入数据库查询实例,这里不限数据库,只要能获取到用户即可。自己new的也行
    @Autowired
    private UserService userService;

    @Override
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 3. 从数据库中查询到 User 对象
        DBUser user = userService.findUserByUsername(username);
        if(user == null){
            throw new AuthenticationCredentialsNotFoundException("user not found.");
        }
        // 4. 将 DBUser 包装为一个 UserDetails 对象,并将 user.roles 转换为 Authority
        return User.withDefaultPasswordEncoder().username(user.username).password(user.password).authorities(user.roles);
    }
}

二、其他

上面几个类大致说明了,如果将数据库的用户转换为 Spring Security 支持的 Authentication 对象的。

其它如内存认证、jdbcDaoImpl 的实现不再赘述。

还有如自定义 PasswordEncoder、自定义 UserDetails 等其它实现,就不一一列举了,网上一搜一大把。

文章作者: koral
文章链接: http://luokaiii.github.io/2019/07/19/读书笔记/《SpringSecurity》/7.核心服务/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自