迎着风跑  

UserDetailsService

#1. 基本概念

  • AuthenticationManager ,类似于 Shiro 中的 SecurityManager 。

    它是 “表面上” 的做认证和鉴权比对工作的那个人,它是认证和鉴权比对工作的起点。

    ProvierderManager 是 AuthenticationManager 的具体实现。

  • AuthenticationProvider ,类似于 Shiro 中的 Authenticator

    它是 “实际上” 的做认证和鉴权比对工作的那个人。从命名上很容易看出,Provider 受 ProviderManager 的管理,ProviderManager 调用 Provider 进行认证和鉴权的比对工作。

    我们最常用到 DaoAuthenticationProvider 是 AuthenticationProvider 的具体实现。

  • UserDetailService ,类似于 Shiro 中的 Realm 。

    虽然 AuthenticationProvider 负责进行用户名和密码的比对工作,但是它并不清楚用户名和密码的『标准答案』,而标准答案则是由 UserDetailService 来提供。简单来说,UserDetailService 负责提供标准答案,以供 AuthenticationProvider 使用。

  • UserDetails,类似于 Shiro 中的 AuthenticationInfo + AuthorizationInfo

    UserDetails 它是存放用户认证信息和权限信息的标准答案的 “容器” ,它也是 UserDetailService “应该” 返回的内容。和 Shiro 不同的是,Shiro 中是把认证和权限信息分开放的,而 UserDetails 则是塞到了一起。

  • PasswordEncoder,类似于 Shiro 中的 CredentialsMatcher 。

    Spring Security 要求密码不能是明文,必须经过加密器加密。这样,AuthenticationProvider 在做比对时,就必须知道『当初』密码时使用哪种加密器加密的。所以,AuthenticationProvider 除了要向 UserDetailsService 『要』用户名密码的标准答案之外,它还需要知道配套的加密算法(加密器)是什么。

#2. UserDetailsService 和 UserDetails

之前有提到过,UserDetailsService 类似于 Shiro 中的 Realm,负责提供用户信息的 “标准答案” ,供 AuthenticationProvider 来比对。Spring Security 提供了 2 个内置的 UserDetailsService 的实现类:InMemoryUserDetailsManager 和 JdbcDaoImpl 。不过在实际项目中,通常我们并不会使用到它俩。

非常别扭的一点是:从名字上,你根本看不出 InMemoryUserDetailsManager 和 JdbcDaoImpl 是 UserDetailsService 的实现类。

如果说 UserDetailsService 的职责类似于 Shiro 中的 Realm,那么 UserDetails 的职责就是 Shiro 中的 AuthenticationInfo + AuthorizationInfo 。

Spring Security 要求 UserDetailsService 将用户信息的 “标准答案” 必须封装到一个 UserDetails 对象中,返回给 AuthenticationProvider 使用(做比对工作)

我们可以直接使用 Spring Security 内置的 UserDetails 的实现类:User 。

public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 模拟注册时的加密效果。
        String password = NoOpPasswordEncoder.getInstance().encode("123");
 
        // 硬编码用户名、密码、角色权限信息。现实工作中并非如此。
        return new User("tom", password, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN")); //  这里 admin 的大小写有区别
  }

}
Copied!

ProviderManager/AuthenticationProvider 在做密码密码的比对工作时,会调用 UserDetailsService 的 .loadUserByUsername() 方法,并传入『用户名』,用以查询该用户的密码和权限信息。

UserDetails 中封装了用户登录过程中所需的全部信息:

方法说明
isAccountNonExpired
isAccountNonLocked
isCredentialsNonExpired
暂时用不到,统一返回 true ,否则 Spring Security 会认为账号异常。
isEnabled 配合数据库层面的逻辑删除功能,用来表示当前用户是否还存在、是否可用。
getPassword
getUsername
需要返回的内容显而易见。
getAuthorities 用于返回用户的权限信息。这里的权限就这是指用户的角色。它的返回值类型是 Collection<? extends GrantedAuthority>,具体形式通常是:List<GrantedAuthority>,里面用来存储角色信息(或权限信息)
return new User("tom", password, 
        true, true, true, true,
        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
Copied!

SimpleGrantedAuthority 是 GrantedAuthority 的一个实现类,也是最常见最常用的和实现类。如果直接使用的话那就是 new SimpleGrantedAuthority("ROLE_USER") 。

注意

另外需要注意的一点是,在一套配置中如果你存在多个 UserDetailsService 的 Spring Bean将会影响 DaoAuthenticationProvider 的注入和使用,从而导致出现 No Provider ... 的异常。

#4. Spring Security 和 RBAC

虽然在 RBAC 模型中,用户的 “权限” 是 “角色” 的下一级,但是在 Spring Security 中,它是将角色和权限一视同仁的,即,Spring Security 不强求你的角色和权限有上下级的关系。

在 Spring Security 中角色和权限都属于 Authority 。不过,Spring Security 有个『人为约定』:

  • 如果你的 Authority 指的是角色,那么角色(的标准答案)就需要以 ROLE_ 开头;

  • 如果你的 Authority 指的是权限,那么权限(的标准答案)则不需要特定的开头。

在后续很多涉及『角色』的地方,Spring Security 都会对 ROLE_ 做额外处理。

#5. 配置使用自定义 UserDetailsService

@Slf4j
@Configuration
@SuppressWarnings("deprecation")
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource // 依赖注入
    private MyUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        
        // 这一步和 Shiro 中的 Realm 和 CredentialsMatcher 的绑定很像,让两者关联。
        auth.userDetailsService(userDetailsService) // 配置 UserDetailService
            .passwordEncoder(passwordEncoder())     // 配置 PasswordEncoder
        ; 
    }
}
Copied!

#6. 最后的说明

这里有 2 点需要说明:

  • 截至目前为止,我们暂时只涉及到了『认证』,还没有涉及到『鉴权』。

  • 截至目前为止,我们还有一些配置没有自定义,仍然使用的是默认配置。

posted on 2021-12-02 10:22  迎着风跑  阅读(992)  评论(0编辑  收藏  举报