IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring Security:用户UserDetails源码与Debug分析 -> 正文阅读

[Java知识库]Spring Security:用户UserDetails源码与Debug分析

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接口,删除了部分settergetter代码)

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;
		// 调用了sortAuthorities方法
		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());
	}

	/**
	 * 构建用户(Builder设计模式)
	 * 至少应提供用户名、密码和权限
	 * 其余属性具有合理的默认值
	 */
	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));
		}

        // 根据UserBuilder实例build新的UserDetails实例(User实例)
		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_前缀),很显然是调用了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需要客户端那边提供用户名和密码用于验证。
在这里插入图片描述
客户端点击登录后,Spring Security会基于用户名去用户服务(以后再介绍,相当于获取用户的媒介,因此可以基于内存、数据库等方式来实现,下图的InMemoryUserDetailsManager类,是一种基于内存的用户服务)查找是否存在该用户(一样的用户名),如果没有,就会抛出异常,否则,会基于查找到的MutableUser实例(用户可能修改密码,需要密码可变,该实例的委托对象正是应用启动时Spring Security基于配置文件创建的User实例)创建新的User实例。
在这里插入图片描述
新的User实例(User@6192)。
在这里插入图片描述
新的User实例(User@6192)会用于与客户端输入的用户信息进行匹配验证,密码目前很显然还不匹配({noop}itkavenitkaven)。
在这里插入图片描述
匹配时,使用extractEncodedPassword方法将User实例的密码{noop}itkaven截取成itkaven,这样密码就匹配了,Spring Security提供了多种密码编码器(根据不同的应用场景),以后博主会详细介绍,因此不要以为密码匹配时总是会将User实例的密码进行截取,这只是一种密码编码器而已。
在这里插入图片描述
extractEncodedPassword方法就是查找密码中}的位置,然后进行截取。

	private static final String SUFFIX = "}";
	
	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}

自动创建用户

Spring Security在没有用户或用户源相关配置时会自动创建用户(应用启动时自动创建,饿汉式),用户名为user,密码是自动生成的(也会加{noop}前缀,验证时也是使用没有{noop}前缀的密码,因为创建的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接口及其实现类的内容以后会详细介绍。

客户端进行登录验证。
在这里插入图片描述
Spring Security通过UserDetailsService实例加载与该用户的用户名匹配的UserDetails实例(懒汉式)。
在这里插入图片描述

因此UserDetails实例(大部分情况下是UserMutableUser实例)是为了验证用户才被创建的(饿汉式与懒汉式),用户进行验证时,Spring Security会通过UserDetailsService实例加载与该用户的用户名匹配的UserDetails实例(也可能是基于查找到的实例重新创建的UserDetails实例,如InMemoryUserDetailsManager类),然后就可以将用户的输入与该UserDetails实例进行匹配,如果匹配成功,则验证成功,否则验证失败。

用户UserDetails源码与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-01-04 13:16:53  更:2022-01-04 13:19:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 8:03:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码