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 的大小写有区别
}
}
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"));
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
;
}
}
#6. 最后的说明
这里有 2 点需要说明:
-
截至目前为止,我们暂时只涉及到了『认证』,还没有涉及到『鉴权』。
-
截至目前为止,我们还有一些配置没有自定义,仍然使用的是默认配置。