栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

在上一篇博主中,博主介绍了Spring Security的UserDetails接口及其实现,Spring Security使用UserDetails实例(实现类的实例)表示用户,当客户端进行验证时(提供用户名和密码),Spring Security会通过用户服务(UserDetailsService接口及其实现)来获取对应的UserDetails实例(相同的用户名),如果该UserDetails实例存在并且与客户端输入的信息匹配,则验证成功,否则验证失败,想了解UserDetails接口及其实现可以看下面这篇博客:

  • Spring Security:用户UserDetails源码与Debug分析
UserDetailsService

UserDetailsService接口源码:

package org.springframework.security.core.userdetails;


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;


public interface UserDetailsManager extends UserDetailsService {

	
	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 {

   
	protected void addCustomAuthorities(String username,
			List authorities) {
	}

    // 通过用户名加载用户的核心逻辑,通过使用其他方法来完成
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		List 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 dbAuthsSet = new HashSet<>();

		if (this.enableAuthorities) {
			dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
		}

		if (this.enableGroups) {
			dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
		}

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

	
	protected List 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);
				});
	}

	
	protected List 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);
				});
	}

	
	protected List loadGroupAuthorities(String username) {
		return getJdbcTemplate().query(this.groupAuthoritiesByUsernameQuery,
				new String[] { username }, (rs, rowNum) -> {
					String roleName = getRolePrefix() + rs.getString(3);

					return new SimpleGrantedAuthority(roleName);
				});
	}

	
	protected UserDetails createUserDetails(String username,
			UserDetails userFromUserQuery, List 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);
	}

	
	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 users = new HashMap<>();
    // 用于处理验证请求,以后会详细介绍
	private AuthenticationManager authenticationManager;

    // 无参构造器
	public InMemoryUserDetailsManager() {
	}

    // 基于用户列表的构造器
	public InMemoryUserDetailsManager(Collection 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源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/697085.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号