Spring Security 教程

Spring Security UserDetailsService 接口

当我们对 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 中提供了多个实现类,如下图:

Spring Security UserDetailsService 接口

我们只需要使用里面的 User 类即可。注意 User 的全限定路径是:

org.springframework.security.core.userdetails.User

注意,此处经常和系统中自己开发的 User 类弄混。

在 User 类中提供了很多方法和属性,如下图:

Spring Security UserDetailsService 接口

其中,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 密码编码器。

application.properties 配置

spring.application.name=spring_security_demo2

#spring.security.user.name=admin
#spring.security.user.password=123456

UserDetailsService 接口实现

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 异常。

配置 Spring Security

要把自定义的 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 注解的类),如下图:

Spring Security UserDetailsService 接口

测试应用

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

Spring Security UserDetailsService 接口

注意,没有样式是因为网络问题,导致加载 bootstrap 样式 css 失败导致的。

在 Username 和 Password 中输入用户名 admin,密码 123456,点击“Sign in”按钮登录,登录成功后界面如下:

Spring Security UserDetailsService 接口

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号