开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 27 天,点击查看活动详情
在之前的一篇文章中,我介绍了如何基于 Apache Shiro 实现 RBAC 访问控制,文章链接:Spring Boot「22」在 Web 应用中基于 JdbcRealm 进行安全验证。 今天我将介绍如何使用 Spring Security 来重新实现之前的例子。
01-如何与数据库中的 RBAC 表关联起来
要理解数据库表与 Spring Security 框架是如何关联起来的,需要先理解框架中的关键接口。
首先是 Authentication 接口,可以对比 Shrio 中的 Subject 理解进行理解。 它包含了 token(用户名、密码等要素信息),也包含了认证成功后的信息,例如权限等;
然后,与 Authentication 紧密相关的是 AuthenticationProvider。 它用来表示实现它的类能够处理某种特定类型的 Authentication。 其中两个方法:
Authentication authenticate(Authentication authentication) throws AuthenticationException;
// 是否支持对某个特定类型的 Authentication
boolean supports(Class<?> authentication);
它的一个基础实现是 AbstractUserDetailsAuthenticationProvider,能够处理 UsernamePasswordAuthenticationToken.class 类及其子类。 它的 supports 方法实现为:
public boolean supports(Class<?> authentication) {
// UsernamePasswordAuthenticationToken 类及其子类
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
它的 authenticate 方法实现为:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
// 缓存中若无,则从 retrieveUser 获得,主要看子类如何实现
if (user == null) {
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
// 省略一些对 UserDetails 的校验步骤
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
// 创建 Authentication 并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider 的子类,并且实现了 retrieveUser 方法。 主要从 UserDetailsService 获取:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;
UserDetailsService 有点类似于 Shiro 中的 Realm,是获取用户信息的 DAO 类。 它只有一个接口,loadUserByUsername 根据用户名,获取用户信息,返回值必须实现 UserDetails 接口。 UserDetailsManager 是对 UserDetailsService 的扩展,增加了“增、删、改”等方法。 Spring 提供了两个 UserDetailsManager 接口实现类,一个是基于内存的 InMemoryUserDetailsManager;另一个是基于 JDBC 的 JdbcUserDetailsManager。
以 JdbcUserDetailsManager 为例,它的 loadUserByUsername 方法实现为:
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据名称获取用户信息,UserDetails 类在下面会介绍
List<UserDetails> users = loadUsersByUsername(username); // 需要适配数据库中的表
UserDetails user = users.get(0); // contains no GrantedAuthority[]
Set<GrantedAuthority> dbAuthsSet = new HashSet<>(); // 获取数据库中的权限信息
if (this.enableAuthorities) {
dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
}
if (this.enableGroups) {
dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
}
List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
addCustomAuthorities(user.getUsername(), dbAuths);
return createUserDetails(username, user, dbAuths);
}
Spring Security 中定义了一个 UserDetails 接口,用来提供关键的用户信息,例如用户名、密码、是否过期等。 Spring Security 框架并不会直接使用实现了 UserDetails 接口的对象,而是会通过这些方法来填充 Authentication 中的属性。 Spring Security 中提供了一个 User 类,它作为 UserDetails 的参考实现。
有了上面的理解后,我们来重新理解下 DaoAuthenticationProvider 这个类。 它是 AuthenticationProvider 的一个实现类,表示他能够对 Authentication 进行认证处理。 从它的父类 AbstractUserDetailsAuthenticationProvider 得知,它能够处理 Authentication 类型是 UsernamePasswordAuthenticationToken 及其子类。 而且,它包含了一个 UserDetailsService,能够从内存、数据库中获得用户的信息并认证,所以它才有 DaoAuthenticationProvider 这个名字,意为基于数据访问对象(内存、数据库)的 AuthenticationProvider。
然后,对比 Shiro 来加深下理解,AuthenticationProvider 接口对标的是 Shiro 中的 Authenticator。 UserDetailsService 接口对标 Shiro 中的 Realm 接口。 与 Spring Boot「22」在 Web 应用中基于 JdbcRealm 进行安全验证 中实现 JPARealm 类似,我们需要基于 JdbcUserDetailsManager 实现一个能从数据库表中读取用户信息的类。
02-基于 JPA 的实现
接下来,我将演示如何基于 JdbcUserDetailsManager 实现一个 UserDetailsManager,它从 RBAC 表(主要是 DEMO_USER / DEMO_USER_PERMISSION / DEMO_PERMISSION)中读取用户、权限信息。
@Bean(name = "userDetailsService")
public JdbcUserDetailsManager jdbcUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
// 通过匿名类,继承 JdbcUserDetailsManager,重写它的相关方法
final JdbcUserDetailsManager manager = new JdbcUserDetailsManager(getDataSource()) {
// loadUsersByUsername 负责从 DEMO_USER 中根据用户名查询用户列表
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
final Optional<DemoUser> user = getUserRepository().findByUsername(username);
return user.<List<UserDetails>>map(Collections::singletonList).orElse(Collections.emptyList());
}
// loadUserAuthorities 根据户名,从 DEMO_PERMISSION / DEMO_USER_PERMISSION 中查询权限列表
@Override
protected List<GrantedAuthority> loadUserAuthorities(String username) {
final List<UserDetails> userDetails = loadUsersByUsername(username);
if (userDetails.size() == 0) {
throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound",
new Object[] { username }, "Username {0} not found"));
}
final DemoUser user = (DemoUser) userDetails.get(0); // 获取用户信息
// 查询用户的权限列表
final List<DemoUserPermission> userPermissions = getUserPermissionRepository().findAllByUserId(user.getUserId());
if (userPermissions.size() == 0) {
throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority",
new Object[] { username }, "User {0} has no GrantedAuthority"));
}
// 根据权限id,查询所有的权限信息
final List<DemoPermission> permissions = getPermissionRepository().findAllById(
userPermissions.stream()
.map(DemoUserPermission::getPermissionId)
.collect(Collectors.toList()));
return new ArrayList<>(permissions);
}
};
manager.setEnableGroups(false);
return manager;
}
为了配合 Spring Security 的接口,我们还需要对 Entity 类进行一点改造,主要是为它们实现特定的接口。
// 主要是因为 loadUserAuthorities 方法返回的是 List<GrantedAuthority>
public class DemoPermission implements GrantedAuthority { /**...*/ }
// 主要是因为 loadUsersByUsername 方法返回的是 List<UserDetails>
public class DemoUser implements UserDetails { /**...*/ }
运行程序,就能发现,我们埋在数据库中的 admin:admin 用户可以登录成功了。 这里使用的演示程序基于 Spring Boot「41」一文搞懂 Spring Security 是如何工作的? 中的 HelloWorld 应用。
03-总结
今天,我介绍了如何使用 Spring Security 中的 JdbcUserDetailsManager 实现基于 RBAC 的认证。 并借助 Shiro 中的概念帮助理解 Spring Security 中的接口。
希望今天的内容能对你有所帮助。