在上一篇博主中,博主介绍了Spring Security
的UserDetails
接口及其实现,Spring Security
使用UserDetails
实例(实现类的实例)表示用户,当客户端进行验证时(提供用户名和密码),Spring Security
会通过用户服务(UserDetailsService
接口及其实现)来获取对应的UserDetails
实例(相同的用户名),如果该UserDetails
实例存在并且与客户端输入的信息匹配,则验证成功,否则验证失败,想了解UserDetails
接口及其实现可以看下面这篇博客:
UserDetailsService
UserDetailsService
接口源码:
package org.springframework.security.core.userdetails;
/**
* 加载用户特定数据的核心接口
* 它在整个框架中作为用户的DAO层(用户数据访问层)
*/
public interface UserDetailsService {
/**
* 根据用户名定位用户
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService
接口只定义了一个方法,即通过用户名查找UserDetails
实例的方法,因此子类可以有各种各样的实现,可以基于JVM
的堆内存(比如使用ConcurrentHashMap
存储UserDetails
实例),或者基于中间件(比如Mysql
、Redis
),或者两者的混合模式,可以根据需求来自定义实现。UserDetailsService
接口的继承与实现关系如下图所示:
UserDetailsManager
UserDetailsManager
接口源码(继承UserDetailsService
接口):
package org.springframework.security.provisioning;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* UserDetailsService的扩展,提供创建新用户和更新现有用户的能力
*/
public interface UserDetailsManager extends UserDetailsService {
/**
* 使用提供的UserDetails实例创建一个新用户
*/
void createUser(UserDetails user);
/**
* 更新指定的用户
*/
void updateUser(UserDetails user);
/**
* 删除给定用户名的用户
*/
void deleteUser(String username);
/**
* 修改当前用户的密码
*/
void changePassword(String oldPassword, String newPassword);
/**
* 检查是否存在给定用户名的用户
*/
boolean userExists(String username);
}
很显然UserDetailsManager
接口是UserDetailsService
接口的扩展,提供了创建新用户和更新现有用户的能力。
JdbcDaoImpl
JdbcDaoImpl
类的结构如下图所示:
使用JDBC
从数据库中检索用户详细信息(用户名、密码、启用标志和权限)。假设有一个默认的数据库模式(有users
和authorities
两个表)。
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
如果数据库模式和默认不一样,可以设置usersByUsernameQuery
和authorityByUsernameQuery
属性以匹配数据库设置,不然它们的值都是默认SQL
。
public JdbcDaoImpl() {
this.usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
this.authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
this.groupAuthoritiesByUsernameQuery = DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY;
}
可以通过将enableGroups
属性设置为true
来启用对组权限的支持(还可以将enableAuthorities
设置为false
以直接禁用权限加载)。 通过这种方法,权限被分配给组,并且用户的权限是根据他们所属的组来确定的。 最终结果是相同的(加载了包含一组GrantedAuthority
的UserDetails
实例)。使用组时,需要表groups
、group_members
和group_authorities
。
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
关于加载组权限的默认查询,可以参考DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY
。 同样,可以通过设置groupAuthoritiesByUsernameQuery
属性来自定义它。
JdbcDaoImpl
类源码(实现了UserDetailsService
接口,提供了使用JDBC
获取用户数据的基本实现,下面代码删除了上面已经提到的内容):
public class JdbcDaoImpl extends JdbcDaoSupport
implements UserDetailsService, MessageSourceAware {
/**
* 允许子类将他们自己授予的权限添加到UserDetail实例的权限列表中
*/
protected void addCustomAuthorities(String username,
List<GrantedAuthority> authorities) {
}
// 通过用户名加载用户的核心逻辑,通过使用其他方法来完成
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
List<UserDetails> users = loadUsersByUsername(username);
if (users.size() == 0) {
this.logger.debug("Query returned no results for user '" + username + "'");
throw new UsernameNotFoundException(
this.messages.getMessage("JdbcDaoImpl.notFound",
new Object[] { username }, "Username {0} not found"));
}
UserDetails user = users.get(0);
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);
if (dbAuths.size() == 0) {
this.logger.debug("User '" + username
+ "' has no authorities and will be treated as 'not found'");
throw new UsernameNotFoundException(this.messages.getMessage(
"JdbcDaoImpl.noAuthority", new Object[] { username },
"User {0} has no GrantedAuthority"));
}
return createUserDetails(username, user, dbAuths);
}
/**
* 通过执行SQL(usersByUsernameQuery)获取UserDetails实例列表
* 通常应该只有一个匹配的用户
*/
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(this.usersByUsernameQuery,
new String[] { username }, (rs, rowNum) -> {
String username1 = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username1, password, enabled, true, true, true,
AuthorityUtils.NO_AUTHORITIES);
});
}
/**
* 通过执行SQL(authorityByUsernameQuery)加载权限
*/
protected List<GrantedAuthority> loadUserAuthorities(String username) {
return getJdbcTemplate().query(this.authoritiesByUsernameQuery,
new String[] { username }, (rs, rowNum) -> {
String roleName = JdbcDaoImpl.this.rolePrefix + rs.getString(2);
return new SimpleGrantedAuthority(roleName);
});
}
/**
* 通过执行SQL(groupAuthoritiesByUsernameQuery)加载权限
*/
protected List<GrantedAuthority> loadGroupAuthorities(String username) {
return getJdbcTemplate().query(this.groupAuthoritiesByUsernameQuery,
new String[] { username }, (rs, rowNum) -> {
String roleName = getRolePrefix() + rs.getString(3);
return new SimpleGrantedAuthority(roleName);
});
}
/**
* 可以重写由loadUserByUsername方法返回的UserDetails实例
*/
protected UserDetails createUserDetails(String username,
UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) {
String returnUsername = userFromUserQuery.getUsername();
if (!this.usernameBasedPrimaryKey) {
returnUsername = username;
}
return new User(returnUsername, userFromUserQuery.getPassword(),
userFromUserQuery.isEnabled(), userFromUserQuery.isAccountNonExpired(),
userFromUserQuery.isCredentialsNonExpired(), userFromUserQuery.isAccountNonLocked(), combinedAuthorities);
}
/**
* 允许指定默认角色前缀
* 如果将其设置为非空值,则它会自动添加到从数据库中读取的任何角色
* 例如,可以用于添加其他Spring Security类默认加在角色名称中的ROLE_前缀,以防该前缀在数据库中不存在
*/
public void setRolePrefix(String rolePrefix) {
this.rolePrefix = rolePrefix;
}
}
JdbcDaoImpl
类提供了使用JDBC
获取用户数据的基本实现。
JdbcUserDetailsManager
JdbcUserDetailsManager
类继承了JdbcDaoImpl
类(提供了使用JDBC
获取用户数据的基本实现),并且实现了UserDetailsManager
和GroupManager
这两个接口。UserDetailsManager
接口提供了创建新用户和更新现有用户的能力。GroupManager
接口允许管理组权限及其成员,通常用于在以下情况下补充UserDetailsManager
的功能:
- 将应用程序授予的权限组织到组中,而不是直接将用户与角色进行映射。
- 在这种情况下,用户被分配到组并获得分配组的权限列表,从而提供更灵活的管理选项。
新增的默认SQL
(UserDetailsManager SQL
和GroupManager SQL
):
// UserDetailsManager SQL
public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";
// GroupManager SQL
public static final String DEF_FIND_GROUPS_SQL = "select group_name from groups";
public static final String DEF_FIND_USERS_IN_GROUP_SQL = "select username from group_members gm, groups g "
+ "where gm.group_id = g.id and g.group_name = ?";
public static final String DEF_INSERT_GROUP_SQL = "insert into groups (group_name) values (?)";
public static final String DEF_FIND_GROUP_ID_SQL = "select id from groups where group_name = ?";
public static final String DEF_INSERT_GROUP_AUTHORITY_SQL = "insert into group_authorities (group_id, authority) values (?,?)";
public static final String DEF_DELETE_GROUP_SQL = "delete from groups where id = ?";
public static final String DEF_DELETE_GROUP_AUTHORITIES_SQL = "delete from group_authorities where group_id = ?";
public static final String DEF_DELETE_GROUP_MEMBERS_SQL = "delete from group_members where group_id = ?";
public static final String DEF_RENAME_GROUP_SQL = "update groups set group_name = ? where group_name = ?";
public static final String DEF_INSERT_GROUP_MEMBER_SQL = "insert into group_members (group_id, username) values (?,?)";
public static final String DEF_DELETE_GROUP_MEMBER_SQL = "delete from group_members where group_id = ? and username = ?";
public static final String DEF_GROUP_AUTHORITIES_QUERY_SQL = "select g.id, g.group_name, ga.authority "
+ "from groups g, group_authorities ga "
+ "where g.group_name = ? "
+ "and g.id = ga.group_id ";
public static final String DEF_DELETE_GROUP_AUTHORITY_SQL = "delete from group_authorities where group_id = ? and authority = ?";
源码就不贴了,太多了,实现方式和JdbcDaoImpl
类差不多(通过使用默认SQL
,也可以使用满足要求的自定义SQL
,查询用户相关数据),需要使用可自行阅读源码(还是要多看源码)。
CachingUserDetailsService
CachingUserDetailsService
类源码(实现了UserDetailsService
接口):
public class CachingUserDetailsService implements UserDetailsService {
// 用户缓存,NullUserCache不执行任何缓存
private UserCache userCache = new NullUserCache();
// 委托的UserDetailsService实例
private final UserDetailsService delegate;
public CachingUserDetailsService(UserDetailsService delegate) {
this.delegate = delegate;
}
public UserCache getUserCache() {
return userCache;
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public UserDetails loadUserByUsername(String username) {
// 从用户缓存中获取用户(通过用户名),而NullUserCache永远返回null
UserDetails user = userCache.getUserFromCache(username);
if (user == null) {
// 通过委托的UserDetailsService实例来获取用户(基于用户名)
user = delegate.loadUserByUsername(username);
}
Assert.notNull(user, () -> "UserDetailsService " + delegate
+ " returned null for username " + username + ". "
+ "This is an interface contract violation");
// 将用户添加到用户缓存,而NullUserCache啥也不会做
userCache.putUserInCache(user);
return user;
}
}
NullUserCache
类,不执行任何缓存。
public class NullUserCache implements UserCache {
public UserDetails getUserFromCache(String username) {
return null;
}
public void putUserInCache(UserDetails user) {
}
public void removeUserFromCache(String username) {
}
}
CachingUserDetailsService
实例可以通过设置不同的用户缓存(以后介绍)实例来达到不同的缓存效果。
// 设置用户缓存实例
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
InMemoryUserDetailsManager
InMemoryUserDetailsManager
通过HashMap
存储用户数据,是一种UserDetailsManager
的非持久化实现。主要用于测试和演示目的,不需要完整的持久化系统。
InMemoryUserDetailsManager
类源码(实现了UserDetailsManager
和UserDetailsPasswordService
接口,UserDetailsPasswordService
接口定义了用于更改UserDetails
密码的方法)
public class InMemoryUserDetailsManager implements UserDetailsManager,
UserDetailsPasswordService {
protected final Log logger = LogFactory.getLog(getClass());
// 存储用户数据的容器
private final Map<String, MutableUserDetails> users = new HashMap<>();
// 用于处理验证请求,以后会详细介绍
private AuthenticationManager authenticationManager;
// 无参构造器
public InMemoryUserDetailsManager() {
}
// 基于用户列表的构造器
public InMemoryUserDetailsManager(Collection<UserDetails> users) {
for (UserDetails user : users) {
createUser(user);
}
}
public InMemoryUserDetailsManager(UserDetails... users) {
for (UserDetails user : users) {
createUser(user);
}
}
// 基于Properties的构造器
public InMemoryUserDetailsManager(Properties users) {
Enumeration<?> names = users.propertyNames();
// UserAttribute编辑器
UserAttributeEditor editor = new UserAttributeEditor();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
editor.setAsText(users.getProperty(name));
// 用于临时存储与用户关联的属性
UserAttribute attr = (UserAttribute) editor.getValue();
// 创建UserDetails实例(User实例)
UserDetails user = new User(name, attr.getPassword(), attr.isEnabled(), true,
true, true, attr.getAuthorities());
// 将用户加入容器
createUser(user);
}
}
// 将用户加入容器
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
// 将UserDetails实例转换成MutableUser实例加入容器
users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
// 将用户从容器中移除
public void deleteUser(String username) {
users.remove(username.toLowerCase());
}
// 更新容器中的指定用户
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
// 将UserDetails实例转换成MutableUser实例用于更新容器
users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
// 判断容器是否存在该用户名的用户
public boolean userExists(String username) {
return users.containsKey(username.toLowerCase());
}
// 修改密码
public void changePassword(String oldPassword, String newPassword) {
// 从SecurityContextHolder中获取需要验证的用户封装,这些以后都会详细介绍
Authentication currentUser = SecurityContextHolder.getContext()
.getAuthentication();
// 没有需要验证的用户
if (currentUser == null) {
throw new AccessDeniedException(
"Can't change password as no Authentication object found in context "
+ "for current user.");
}
// 需要验证的用户的用户名
String username = currentUser.getName();
logger.debug("Changing password for user '" + username + "'");
// 如果已设置AuthenticationManager,使用提供的密码重新验证用户
if (authenticationManager != null) {
logger.debug("Reauthenticating user '" + username
+ "' for password change request.");
// 验证oldPassword是否是该用户的密码
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
username, oldPassword));
}
else {
logger.debug("No authentication manager set. Password won't be re-checked.");
}
// 在容器中查找该用户(基于用户名)
MutableUserDetails user = users.get(username);
// 容器中没有该用户
if (user == null) {
throw new IllegalStateException("Current user doesn't exist in database.");
}
// 以上条件都满足,可以修改密码
user.setPassword(newPassword);
}
// 更新密码
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
// 在容器中查找该用户(基于用户名)
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
// 给该用户设置新密码
mutableUser.setPassword(newPassword);
return mutableUser;
}
// 加载用户(基于用户名)
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 在容器中查找该用户(基于用户名)
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
// 基于在容器中查找到的用户创建User实例
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
// 设置AuthenticationManager,用于处理验证请求
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
自定义用户服务
自定义用户服务:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置自定义的用户服务
// 配置密码编码器(一个什么都不做的密码编码器,用于测试)
auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
// 自定义的用户服务
public static class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟在数据库中查找用户...
// 假设用户存在,并且密码为itkaven,角色列表为USER、ADMIN
UserDetails userDetails = User.withUsername(username).password("itkaven").roles("USER", "ADMIN").build();
return userDetails;
}
}
}
Spring Security
的用户服务UserDetailsService
源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。