当我们对 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”按钮登录,登录成功后界面如下: