在上一篇博主中,博主介绍了Spring Security
的UserDetails
接口及其实现,Spring Security
使用UserDetails
实例(实现类的实例)表示用户,当客户端进行验证时(提供用户名和密码),Spring Security
会通过用户服务(UserDetailsService
接口及其实现)来获取对应的UserDetails
实例(相同的用户名),如果该UserDetails
实例存在并且与客户端输入的信息匹配,则验证成功,否则验证失败,想了解UserDetails
接口及其实现可以看下面这篇博客:
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
接口源码(继承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
类的结构如下图所示:
使用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
类继承了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
类源码(实现了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
通过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
源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。