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

Spring Security:用户UserDetails源码分析

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

Spring Security:用户UserDetails源码分析

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

应用的pom.xml:



    4.0.0

    com.kaven
    security
    1.0-SNAPSHOT

    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.1.RELEASE
    

    
        8
        8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.projectlombok
            lombok
        
    

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 {
	
	Collection getAuthorities();

	
	String getPassword();

	
	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 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类源码(实现UserDetails和CredentialsContainer接口,删除了部分setter、getter代码)

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 authorities;
	// 用户账号是否过期
	private final boolean accountNonExpired;
    // 用户账号是否锁定
	private final boolean accountNonLocked;
	// 用户的凭据是否过期
	private final boolean credentialsNonExpired;
	// 用户的账号是否禁用
	private final boolean enabled;
	
	
	public User(String username, String password,
			Collection authorities) {
		this(username, password, true, true, true, true, authorities);
	}

	public User(String username, String password, boolean enabled,
			boolean accountNonExpired, boolean credentialsNonExpired,
			boolean accountNonLocked, Collection 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 sortAuthorities(
			Collection authorities) {
		Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
		// 确保用户的授权集合的迭代顺序是可预测的
		SortedSet 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,
			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());
		}
	}

	
	@Override
	public boolean equals(Object rhs) {
		if (rhs instanceof User) {
			return username.equals(((User) rhs).username);
		}
		return false;
	}

	
	@Override
	public int hashCode() {
		return username.hashCode();
	}


	
	public static UserBuilder withUsername(String username) {
		return builder().username(username);
	}

	
	public static UserBuilder builder() {
		return new 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 authorities;
		private boolean accountExpired;
		private boolean accountLocked;
		private boolean credentialsExpired;
		private boolean disabled;
		private Function passwordEncoder = password -> password;

		
		public UserBuilder roles(String... roles) {
			List 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 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) {
        User user = properties.getUser();
        List 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}前缀,验证时也是使用没有{noop}前缀的密码)。

配置用户源

添加数据库依赖:

        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            mysql
            mysql-connector-java
        

添加数据库配置:

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实例(大部分情况下是User实例)是为了验证用户才被创建的(饿汉式与懒汉式),用户进行验证时,会通过UserDetailsService实例加载与该用户的用户名匹配的UserDetails实例,然后就可以将用户的输入与UserDetails实例进行匹配,如果匹配成功,则验证成功,否则验证失败。

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

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

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

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