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 {
Collection<? extends GrantedAuthority> 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;
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 类源码(实现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<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
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));
}
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());
}
}
@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<GrantedAuthority> authorities;
private boolean accountExpired;
private boolean accountLocked;
private boolean credentialsExpired;
private boolean disabled;
private Function<String, String> passwordEncoder = password -> password;
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_ 前缀),很显然是调用了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}itkaven 和itkaven )。 匹配时,使用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 {
UserDetails userDetails = User.withUsername(username).password("itkaven").roles("USER", "ADMIN").build();
return userDetails;
}
}
}
当添加了用户源与用户服务的相关配置后,Spring Security 便不会在启动时就创建用户(前两种方式会在启动时就创建用户),因为Spring Security 不可能在启动时就将用户源中的所有用户都创建一次(饿汉),这是不现实的,所以需要自定义用户服务,用户服务就是为了在适当的时机(比如登录验证时)从用户源(数据库、内存等)中加载指定用户(通过用户名,用户源也可能没有该用户),关于UserDetailsService 接口及其实现类的内容以后会详细介绍。
客户端进行登录验证。 Spring Security 通过UserDetailsService 实例加载与该用户的用户名匹配的UserDetails 实例(懒汉式)。
因此UserDetails 实例(大部分情况下是User 和MutableUser 实例)是为了验证用户才被创建的(饿汉式与懒汉式),用户进行验证时,Spring Security 会通过UserDetailsService 实例加载与该用户的用户名匹配的UserDetails 实例(也可能是基于查找到的实例重新创建的UserDetails 实例,如InMemoryUserDetailsManager 类),然后就可以将用户的输入与该UserDetails 实例进行匹配,如果匹配成功,则验证成功,否则验证失败。
用户UserDetails 源码与Debug 分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。
|