0
点赞
收藏
分享

微信扫一扫

Spring Security:用户UserDetails源码分析

Ewall_熊猫 2022-01-04 阅读 86

Spring Security身份验证与授权的对象是用户,这里说的用户可以是配置文件中定义的用户,也可以是数据库表中存储的用户,还可以是Spring Security自动创建的用户(Spring Security在没有用户或用户源相关配置时会自动创建用户),Spring Security使用UserDetails接口来抽象用户。

应用的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaven</groupId>
    <artifactId>security</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

UserDetails

UserDetails接口源码(用户的抽象):

package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;
import java.util.Collection;

/**
 * 提供用户信息
 */
public interface UserDetails extends Serializable {
	/**
	 * 返回授予用户的权限
	 * 不能返回null
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 返回密码
	 */
	String getPassword();

	/**
	 * 返回用于验证用户的用户名
	 * 不能返回null
	 */
	String getUsername();

	/**
	 * 指示用户帐号是否已过期
	 * 无法对过期的用户进行身份验证
	 */
	boolean isAccountNonExpired();

	/**
	 * 指示用户账号是否被锁定
	 * 无法对锁定的用户进行身份验证
	 */
	boolean isAccountNonLocked();

	/**
	 * 指示用户的凭据(密码)是否已过期
	 * 过期的凭据会阻止身份验证
	 */
	boolean isCredentialsNonExpired();

	/**
	 * 指示用户账号是否禁用
	 * 无法对禁用的用户进行身份验证
	 */
	boolean isEnabled();
}

UserDetails接口的继承与实现关系如下图所示:
在这里插入图片描述

MutableUserDetails

MutableUserDetails接口源码(可变用户的抽象,继承UserDetails接口):

package org.springframework.security.provisioning;
import org.springframework.security.core.userdetails.UserDetails;

interface MutableUserDetails extends UserDetails {
    // 设置密码
	void setPassword(String password);
}

MutableUser

MutableUser类源码(可变用户的实现,实现MutableUserDetails接口):

package org.springframework.security.provisioning;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;

class MutableUser implements MutableUserDetails {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 密码
	private String password;
	// 将方法实现委托给其他的UserDetails实例
	private final UserDetails delegate;

	MutableUser(UserDetails user) {
		this.delegate = user;
		this.password = user.getPassword();
	}

	public String getPassword() {
		return password;
	}

    // 设置密码
	public void setPassword(String password) {
		this.password = password;
	}
	
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return delegate.getAuthorities();
	}

	public String getUsername() {
		return delegate.getUsername();
	}

	public boolean isAccountNonExpired() {
		return delegate.isAccountNonExpired();
	}

	public boolean isAccountNonLocked() {
		return delegate.isAccountNonLocked();
	}

	public boolean isCredentialsNonExpired() {
		return delegate.isCredentialsNonExpired();
	}

	public boolean isEnabled() {
		return delegate.isEnabled();
	}
}

MutableUser类只是提供了密码的获取与设置,其他方法的实现委托给了另外的UserDetails实例。

User

User类源码(实现UserDetailsCredentialsContainer接口,删除了部分模板代码)

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

public class User implements UserDetails, CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);

    // 密码
	private String password;
	// 用户名
	private final String username;
	// 用户的授权集合
	private final Set<GrantedAuthority> authorities;
	// 用户账号是否过期
	private final boolean accountNonExpired;
    // 用户账号是否锁定
	private final boolean accountNonLocked;
	// 用户的凭据是否过期
	private final boolean credentialsNonExpired;
	// 用户的账号是否禁用
	private final boolean enabled;
	
	/**
	 * 调用更复杂的构造函数,并将所有布尔参数设置为true
	 */
	public User(String username, String password,
			Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true, true, true, true, authorities);
	}

	public User(String username, String password, boolean enabled,
			boolean accountNonExpired, boolean credentialsNonExpired,
			boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

		if (((username == null) || "".equals(username)) || (password == null)) {
			throw new IllegalArgumentException(
					"Cannot pass null or empty values to constructor");
		}

		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}
	
    // 删除凭据,CredentialsContainer接口定义了该方法
	public void eraseCredentials() {
		password = null;
	}

	private static SortedSet<GrantedAuthority> sortAuthorities(
			Collection<? extends GrantedAuthority> authorities) {
		Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
		// 确保用户的授权集合的迭代顺序是可预测的
		SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
				new AuthorityComparator());

		for (GrantedAuthority grantedAuthority : authorities) {
			Assert.notNull(grantedAuthority,
					"GrantedAuthority list cannot contain any null elements");
			sortedAuthorities.add(grantedAuthority);
		}

		return sortedAuthorities;
	}

	private static class AuthorityComparator implements Comparator<GrantedAuthority>,
			Serializable {
		private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

		public int compare(GrantedAuthority g1, GrantedAuthority g2) {
			// 两者都不应该为空,因为在将每个条目添加到集合之前会检查每个条目是否为空
			// 如果权限为空,则为自定义权限,应优先于其他权限
			if (g2.getAuthority() == null) {
				return -1;
			}

			if (g1.getAuthority() == null) {
				return 1;
			}

			return g1.getAuthority().compareTo(g2.getAuthority());
		}
	}

	/**
	 * 如果提供的对象是具有相同username值的User实例,则返回true
	 * 如果对象具有相同的username值,代表相同的主体,则它们是相等的
	 */
	@Override
	public boolean equals(Object rhs) {
		if (rhs instanceof User) {
			return username.equals(((User) rhs).username);
		}
		return false;
	}

	/**
	 * 返回username的哈希码
	 */
	@Override
	public int hashCode() {
		return username.hashCode();
	}


	/**
	 * 创建具有指定用户名的UserBuilder
	 */
	public static UserBuilder withUsername(String username) {
		return builder().username(username);
	}

	/**
	 * 创建UserBuilder
	 */
	public static UserBuilder builder() {
		return new UserBuilder();
	}

    /**
	 * 基于UserDetails,创建UserBuilder
	 */
    public static UserBuilder withUserDetails(UserDetails userDetails) {
		return withUsername(userDetails.getUsername())
			.password(userDetails.getPassword())
			.accountExpired(!userDetails.isAccountNonExpired())
			.accountLocked(!userDetails.isAccountNonLocked())
			.authorities(userDetails.getAuthorities())
			.credentialsExpired(!userDetails.isCredentialsNonExpired())
			.disabled(!userDetails.isEnabled());
	}

	/**
	 * 构建要添加的用户
	 * 至少应提供用户名、密码和权限
	 * 其余属性具有合理的默认值
	 */
	public static class UserBuilder {
		private String username;
		private String password;
		private List<GrantedAuthority> authorities;
		private boolean accountExpired;
		private boolean accountLocked;
		private boolean credentialsExpired;
		private boolean disabled;
		private Function<String, String> passwordEncoder = password -> password;

		/**
		 * 填充角色
		 * 此方法是调用authorities(String...)的快捷方式,但会自动为每个条目添加“ROLE_”前缀
		 * 这意味着以下内容:builder.roles("USER","ADMIN")
		 * 相当于builder.authorities("ROLE_USER","ROLE_ADMIN")
		 * 此属性是必需的,但也可以使用authorities(String...)填充
		 */
		public UserBuilder roles(String... roles) {
			List<GrantedAuthority> authorities = new ArrayList<>(
					roles.length);
			for (String role : roles) {
				Assert.isTrue(!role.startsWith("ROLE_"), () -> role
						+ " cannot start with ROLE_ (it is automatically added)");
				authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
			}
			return authorities(authorities);
		}

        public UserBuilder authorities(GrantedAuthority... authorities) {
			return authorities(Arrays.asList(authorities));
		}

		public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
			this.authorities = new ArrayList<>(authorities);
			return this;
		}
		
		public UserBuilder authorities(String... authorities) {
			return authorities(AuthorityUtils.createAuthorityList(authorities));
		}

		public UserDetails build() {
			String encodedPassword = this.passwordEncoder.apply(password);
			return new User(username, encodedPassword, !disabled, !accountExpired,
					!credentialsExpired, !accountLocked, authorities);
		}
	}
}

配置文件指定用户

配置文件如下所示:

spring:
  security:
    user:
      name: kaven
      password: itkaven
      roles:
        - USER
        - ADMIN

Debug启动应用,User类的构造器会被调用,如下图所示:
在这里插入图片描述
为什么密码是{noop}itkaven,而不是itkaven(验证时还是需要使用itkaven),是因为在创建User实例之前,密码已经在UserDetailsServiceAutoConfiguration类的getOrDeducePassword方法中被修改了(加{noop}前缀)。

    private String getOrDeducePassword(User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
        }

        return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
    }

并且该用户被授予的权限与配置文件一致,只是role的名称被修改了而已(加了ROLE_前缀),很显然是调用了UserBuilder类的roles方法(在UserDetailsServiceAutoConfiguration类的inMemoryUserDetailsManager方法中被调用)。

    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

自动创建用户

Spring Security在没有用户或用户源相关配置时会自动创建用户,用户名为user,密码是自动生成的(也会加{noop}前缀)。
在这里插入图片描述

配置用户源

添加数据库依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

添加数据库配置:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ITkaven@666.com
    url: jdbc:mysql://192.168.31.150:3306/user?useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
@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便不会在启动时就创建用户(前两种方式会在启动时就创建用户),因为Spring Security不可能在启动时将用户源中的所有用户都创建一次(饿汉),这是不现实的,所以需要自定义用户服务,用户服务就是为了在适当的时机(比如登录验证时)从用户源中加载指定用户(通过用户名,用户源也可能没有该用户),关于UserDetailsService的内容以后会详细介绍。用户UserDetails源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

举报

相关推荐

0 条评论