当我们对 Spring Security 什么也没有配置的时候,账号和密码是由 Spring Security 自动生成的(用户名为 user,密码为随机密码)。而在实际项目中,账号和密码都是从数据库(mysql、oracle 等)中查询出来的。 因此,我们需要通过自定义逻辑控制认证逻辑,在自定义逻辑之前,需要先理解 UserDetailsService 接口。
UserDetailsService 接口是 Spring Security 框架里极为关键的一个接口,其核心功能是从存储(如数据库、内存等)中加载用户的详细信息,进而为身份验证提供所需的数据。如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。
UserDetailsService 接口仅包含一个 loadUserByUsername() 方法:
package org.springframework.security.core.userdetails;
/**
* 加载用户特定数据的核心接口。
* <p>
* 它在整个框架中用作用户数据访问对象(DAO),并且是
* org.springframework.security.authentication.dao.DaoAuthenticationProvider
* 所使用的策略。
*
* <p>
* 该接口只需要一个只读方法,这简化了对新的数据访问策略的支持。
*
* @author Ben Alex
* @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
* @see UserDetails
*/
public interface UserDetailsService {
/**
* 根据用户名查找用户。在实际实现中,搜索可能区分大小写,也可能不区分大小写,
* 这取决于实现实例的配置方式。
* 在这种情况下,返回的 UserDetails 对象的用户名大小写可能与实际请求的不同。
* @param username 标识需要其数据的用户的用户名。
* @return 一个完整填充的用户记录(永远不为 null)
* @throws UsernameNotFoundException 如果找不到用户,或者用户没有GrantedAuthority权限
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}下面将对上述 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 方法进行详细说明:
loadUserByUsername(String username) 方法 username 参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。
loadUserByUsername(String username) 方法的返回值 UserDetails 也是一个接口。
UserDetails 接口定义了一系列方法,用于获取用户的基本信息和权限信息,源码如下:
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* 提供核心用户信息。
*
* <p>
* Spring Security 不会直接将这些实现用于安全目的。
* 它们仅仅存储用户信息,这些信息随后会被封装到 {@link Authentication} 对象中。
* 这使得与安全无关的用户信息(例如电子邮件地址、电话号码等)能够存储在一个方便的位置。
* <p>
* 具体实现必须格外注意,确保为每个方法详细说明的非空契约得以实施。
* 有关参考实现,请参阅{@link org.springframework.security.core.userdetails.User}
* (你可能希望在代码中扩展或使用它)。
*
* @author Ben Alex
* @see UserDetailsService
* @see UserCache
*/
public interface UserDetails extends Serializable {
/**
* 返回授予用户的权限。不能返回<code>null</code>。
* @return 返回的权限,按自然键排序(永远不为<code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 返回用于对用户进行身份验证的密码。
* @return the password
*/
String getPassword();
/**
* 返回用于对用户进行身份验证的用户名。不能返回 <code>null</code>。
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* 指示用户账户是否已过期。已过期的账户无法进行身份验证。
* @return 如果用户账户有效(即未过期),则返回<code>true</code>;
* 如果不再有效(即已过期),则返回<code>false</code>
*/
default boolean isAccountNonExpired() {
return true;
}
/**
* 指示用户是被锁定还是未被锁定。被锁定的用户无法进行身份验证。
* @return 如果用户未被锁定,则返回<code>true</code>,否则返回<code>false</code>
*/
default boolean isAccountNonLocked() {
return true;
}
/**
* 指示用户的凭据(密码)是否已过期。过期的凭据将阻止身份验证。
* @return 如果用户的凭证有效(即未过期),则返回<code>true</code>,
* 如果不再有效(即已过期),则返回<code>false</code>
*/
default boolean isCredentialsNonExpired() {
return true;
}
/**
* 指示用户是启用还是禁用。禁用的用户无法进行身份验证。
* @return 如果用户已启用则返回<code>true</code>,否则返回<code>false</code>
*/
default boolean isEnabled() {
return true;
}
}如果要想返回 UserDetails 的实例,则只能返回接口的实现类。Spring Security 中提供了多个实现类,如下图:

我们只需要使用里面的 User 类即可。注意 User 的全限定路径是:
org.springframework.security.core.userdetails.User
注意,此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性,如下图:

其中,User 有两个构造方法,调用其中任何一个都可以实例化 UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。源码如下:
/**
* 调用更复杂的构造函数,将所有布尔参数都设置为{@code true}。
*/
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
/**
* 使用 <code>org.springframework.security.authentication.dao.
* DaoAuthenticationProvider</code> 所需的详细信息构造 <code>User</code>。
*
* @param username 提供给 <code>DaoAuthenticationProvider</code> 的用户名
* @param password 应提供给 <code>DaoAuthenticationProvider</code> 的密码
* @param enabled 如果用户已启用,则设置为<code>true</code>
* @param accountNonExpired 如果账户未过期,则设置为<code>true</code>
* @param credentialsNonExpired 如果凭证尚未过期,则设置为<code>true</code>
* @param accountNonLocked 如果账户未锁定,则设置为<code>true</code>
* @param authorities 如果调用者提供了正确的用户名和密码,且用户已启用,应授予调用者的权限。不能为空。
* @throws IllegalArgumentException 如果传递了<code>null</code>值,无论是作为
* 参数,还是作为<code>GrantedAuthority</code>集合中的一个元素
*/
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"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));
}注意,此处的 username 应该是客户端传递过来的用户名,而 password 应该是从数据库中查询出来的密码(进行加密后的密码)。Spring Security 会根据 User 中的 password 和客户端传递过来的 password进行比较(注意,客户端传递的密码是明文,User 中的密码是密文,来自数据库)。如果相同则表示认证通过,如果不相同表示认证失败。
Collection<? extends GrantedAuthority> authorities 里面的权限为此用户具有的权限,如果 authorities 里面没有包含权限 x,而用户在做某个事情时必须包含 x 权限,此时会出现 403。
💡小技巧:我们通常都是通过 AuthorityUtils.commaSeparatedStringToAuthorityList("") 来创建authorities 集合对象的,参数是一个字符串,多个权限使用逗号分隔。
loadUserByUsername(String username) 方法明确定义了抛出 UsernameNotFoundException 异常, 当该方法传递的 username(用户名)没有找到时,则抛出 UsernameNotFoundException 异常,系统就知道用户名没有查询到。
以下是一个简单的示例:
(1)创建 UserDetailsService 实现类,实现 UserDetailsService 接口,并重写 loadUserByUsername() 方法。
(2)加载用户信息,在 loadUserByUsername() 方法中,从数据库里加载用户的详细信息,为了演示效果,这里将模拟用户名和密码。
(3)创建 UserDetails 对象,将加载的用户信息封装成 UserDetails 对象并返回。
(4)配置 Spring Security,把自定义的 UserDetailsService 配置到 Spring Security 中。注意,还要记得配置 BCryptPasswordEncoder 密码编码器。
spring.application.name=spring_security_demo2 #spring.security.user.name=admin #spring.security.user.password=123456
package com.hxstrive.spring_security.config;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义实现 UserDetailsService 接口
* @author hxstrive.com
*/
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("loadUserByUsername(String username) :: " + username);
// 模拟从数据库中加载用户信息
// 后面章节将详细介绍如何从数据中加载数据
if ("admin".equals(username)) {
// 创建用户权限列表
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// 创建 UserDetails 对象
// 注意,密码通过 BCryptPasswordEncoder 进行加密获得
// 正常情况下,密码从数据库查询
String password = new BCryptPasswordEncoder().encode("123456");
return new User(username, password, authorities);
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}上述代码,实现了 UserDetailsService 接口,在 loadUserByUsername() 方法中模拟从数据库加载用户信息,若用户存在则返回 UserDetails 对象,不存在则抛出 UsernameNotFoundException 异常。
要把自定义的 UserDetailsService 配置到 Spring Security 中,如下:
package com.hxstrive.spring_security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Spring Security 配置类
* @author hxstrive.com
*/
@Configuration
public class SecurityConfig {
// 配置自定义的 UserDetailsService
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
// 配置密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
// formLogin() 用于配置表单登录功能
// AbstractAuthenticationFilterConfigurer::permitAll 表示允许所有用户访问表单登录页面
.formLogin(AbstractAuthenticationFilterConfigurer::permitAll)
// httpBasic() 启用 HTTP 基本认证
// withDefaults() 表示使用默认的 HTTP 基本认证配置
.httpBasic(withDefaults());
return http.build();
}
}上述代码中,配置了 Spring Security,将自定义的 UserDetailsService 和 PasswordEncoder 注入到 Spring Security 中,同时定义了请求的访问规则和登录方式。
通过上述步骤,你就能够使用自定义的 UserDetailsService 来实现用户的身份验证和权限控制。
在 IDE 中运行 Spring Boot 应用程序的主类(通常是带有 @SpringBootApplication 注解的类),如下图:

打开浏览器,访问 http://localhost:8080/hello 地址,由于所有请求都需要身份验证,你会看到一个登录页面,如下图:

注意,没有样式是因为网络问题,导致加载 bootstrap 样式 css 失败导致的。
在 Username 和 Password 中输入用户名 admin,密码 123456,点击“Sign in”按钮登录,登录成功后界面如下:

spring security负责对密码进行校验,不需要开发者手动校验密码
是的,通过 PasswordEncoder 对密码进行加密/解密。