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
类源码(实现UserDetails
和CredentialsContainer
接口,删除了部分模板代码)
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
源码分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。