Spring Security:用户服务UserDetailsService源码分析

在上一篇博主中,博主介绍了Spring SecurityUserDetails接口及其实现,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实例),或者基于中间件(比如MysqlRedis),或者两者的混合模式,可以根据需求来自定义实现。UserDetailsService接口的继承与实现关系如下图所示:
Spring Security:用户服务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类的结构如下图所示:
Spring Security:用户服务UserDetailsService源码分析
使用JDBC从数据库中检索用户详细信息(用户名、密码、启用标志和权限)。假设有一个默认的数据库模式(有usersauthorities两个表)。

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);

如果数据库模式和默认不一样,可以设置usersByUsernameQueryauthorityByUsernameQuery属性以匹配数据库设置,不然它们的值都是默认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以直接禁用权限加载)。 通过这种方法,权限被分配给组,并且用户的权限是根据他们所属的组来确定的。 最终结果是相同的(加载了包含一组GrantedAuthorityUserDetails实例)。使用组时,需要表groupsgroup_membersgroup_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获取用户数据的基本实现),并且实现了UserDetailsManagerGroupManager这两个接口。UserDetailsManager接口提供了创建新用户和更新现有用户的能力。GroupManager接口允许管理组权限及其成员,通常用于在以下情况下补充UserDetailsManager的功能:

  • 将应用程序授予的权限组织到组中,而不是直接将用户与角色进行映射。
  • 在这种情况下,用户被分配到组并获得分配组的权限列表,从而提供更灵活的管理选项。

新增的默认SQLUserDetailsManager SQLGroupManager 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类源码(实现了UserDetailsManagerUserDetailsPasswordService 接口,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源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

上一篇:ACM-ICPC寒假算法训练1:搜索:一道被输入方式卡住的一道简单题(方法多)


下一篇:递归