目录
- 1.认识UserDetailsService
- 1.1.认识UserDetails
- 1.2.UserDetailsService的默认实现 -- InMemoryUserDetailsManager
- 2.用户信息存储在MySQL数据库中
- 2.1.添加依赖
- 2.2.配置MySQL和Mybatis
- 2.3.在数据库中添加用户信息
- 2.4.添加数据库实体类
- 2.5.编写Mybatis代码
- 2.6.实现UserDetails接口
- 2.7.实现UserDetailsService接口
- 2.8.编写配置文件
- 2.9.编写控制器
- 2.10.单元测试
本章目标:
- 认识UserDetailsService
- 尝试使用MySQL储存用户信息
1.认识UserDetailsService
如图所示,Authentication Filter将身份验证请求委托给AuthenticationManager,后者使用AuthenticationProvider处理身份验证。
Authentication Provider使用UserDetailsService来实现用户管理职责。它的主要职责是根据用户名从内存中或数据库中查找用户。
package org.springframework.security.core.userdetails;
/**
* 加载用户特定数据的核心接口。
* 它作为用户DAO在整个框架中使用,并且是DaoAuthenticationProvider使用的策略。
*
* 该接口只需要一个只读方法,这简化了对新数据访问策略的支持。
*/
public interface UserDetailsService {
/**
* 根据用户名定位用户。在实际实现中,搜索可能区分大小写,也可能不区分大小写,这取决于如何配置实现实例。
* 在这种情况下,返回的UserDetails对象可能有一个不同于实际请求的用户名。
*
* 参数 - username:标识需要其数据的用户的用户名。
* 返回值 - 完全填充的用户记录(绝不为空)
* 异常 - UsernameNotFoundException 如果找不到用户或用户没有授予权限
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
1.1.认识UserDetails
UserDetails是Spring Security框架中的一个核心接口,用于表示用户的详细信息。
UserDetails接口定义在org.springframework.security.core.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;
/*
* 提供核心用户信息。
*
* Spring Security不会出于安全目的直接使用实现。它们只是存储用户信息,这些信息稍后封装到Authentication对象中。
* 这允许非安全相关的用户信息(如电子邮件地址,电话号码等)存储在一个方便的位置。
*
* 具体实现必须特别注意确保执行每个方法的非空契约。有关参考实现(您可能希望在代码中扩展或使用),
* 请参阅org.springframework.security.core.userdetails.User。
*
*/
public interface UserDetails extends Serializable {
/**
* 返回授予用户的权限。不能返回null。
* 返回值:按自然键排序的权限集(绝不为空)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 返回用于验证用户身份的密码。
*/
String getPassword();
/**
* 返回用于验证用户的用户名。不能返回null。
*/
String getUsername();
/**
* 用户帐号是否过期。过期的帐户无法进行身份验证。
* 返回值:如果用户的帐户有效(即未过期)则为true,如果不再有效(即过期)则为false
*/
boolean isAccountNonExpired();
/**
* 显示用户处于锁定状态或未锁定状态。被锁定的用户无法进行认证。
* 返回值:如果用户未被锁定则返回true,否则返回false
*/
boolean isAccountNonLocked();
/**
* 指示用户的凭据(密码)是否已过期。过期凭据阻止身份验证。
* 返回值:如果用户的凭据有效(即未过期)则返回true,如果不再有效(即过期)则返回false
*/
boolean isCredentialsNonExpired();
/**
* 显示用户是否启用或禁用。禁用的用户无法进行认证。
* 返回值:如果用户被启用,返回true,否则返回false
*/
boolean isEnabled();
}
1.2.UserDetailsService的默认实现 – InMemoryUserDetailsManager
InMemoryUserDetailsManager是UserDetailsManager的非持久性实现,他将用户信息存储在内存中,断电后信息会丢失。
通过查看org.springframework.security.provisioning.InMemoryUserDetailsManager
的源代码,可以知道这个实现类包含如下功能:
private final Map<String, MutableUserDetails> users
:使用map将数据存储在内存中SecurityContextHolderStrategy
是Spring Security框架中的一个接口,用于定义如何存储和管理SecurityContext。SecurityContext是Spring Security中用于存储当前用户安全信息的容器,而SecurityContextHolderStrategy则定义了SecurityContext的存储策略。-
AuthenticationManager
在Spring Security中的作用是处理认证请求,并协调多个AuthenticationProvider进行用户凭证的验证。AuthenticationManager是一个接口,定义了认证流程的核心方法,主要负责将Authentication对象传递给合适的AuthenticationProvider进行认证。如果认证成功,它会返回一个包含认证成功信息的Authentication对象;如果失败,则会抛出异常 - 通过构造函数创建新用户
public void createUser(UserDetails user)
:创建不存在的用户public void deleteUser(String username)
:删除用户public void updateUser(UserDetails user)
:更新已存在的用户public boolean userExists(String username)
:用户是否存在public void changePassword(String oldPassword, String newPassword)
:修改当前登录的用户的密码public UserDetails updatePassword(UserDetails user, String newPassword)
:更新指定用户的密码public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
:通过用户名加载用户信息,用于认证public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy)
:配置SecurityContextHolderStrategy
public void setAuthenticationManager(AuthenticationManager authenticationManager)
:配置AuthenticationManager
2.用户信息存储在MySQL数据库中
2.1.添加依赖
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
2.2.配置MySQL和Mybatis
application-dev.properties
编写开发环境配置文件
# 配置MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ipms?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
# 配置Mybatis
mybatis.mapper-locations=classpath:/mapper/*.xml
mybatis.type-aliases-package=com.drson.usermanagement.entity
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
application.properties
编写主配置文件
spring.application.name=userManagement
# 激活开发环境配置文件
spring.profiles.active=dev
2.3.在数据库中添加用户信息
# 在数据库中添加用户信息
# 创建数据库
create database if not exists ipms character set utf8mb4 collate utf8mb4_unicode_ci;
# 选择数据库
use ipms;
# 创建用户表
create table if not exists employee
(
id int auto_increment primary key comment '员工ID',
job_number varchar(8) not null unique comment '工号',
name varchar(10) not null comment '姓名',
gender tinyint not null comment '性别:1为男性,0为女性',
age tinyint not null comment '年龄',
phone varchar(11) not null comment '电话号码',
address varchar(255) not null comment '住址',
id_card_number varchar(20) not null comment '身份证号码',
educational_background varchar(4) not null comment '学历',
password varchar(60) not null comment '登录管理系统的密码',
isAccountNonExpired boolean not null comment '账号是否过期',
isAccountNonLocked boolean not null comment '账号是否被锁定',
isCredentialsNonExpired boolean not null comment '密码是否过期',
isEnabled boolean not null comment '账户是否可用'
);
# 添加一个用户,用于测试,密码123456
insert into employee (job_number, name, gender, age, phone, address, id_card_number, educational_background, password,
isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled) VALUE
(
'00000000',
'test',
1,
30,
'18888888888',
'测试住址',
'511133197301240418',
'大专',
'$2y$16$xvsERHKdRrdDgLJeLdthFuLk/Tlv1rpfuGpftC8PDpZwZzCAfxq9i',
true, true, true, true
);
# 查询表格
select * from employee;
其中的加密后的密码$2y$04$Z6tlj.KTXI5AEEmgGPZ1J.Gxq..h8OSyUF30bku551ImynFVlQnw6
,使用下面的测试代码生成:
@Test
public void encodePassword(){
/*
* BCryptPasswordEncoder()的参数说明:
* strength:定义了哈希计算的时间因子(以 rounds 表示),其中值越大,计算哈希所需的时间就越长,从而增加了攻击者破解密码的难度。默认值是10,范围4-31
* version:用于指定BCrypt算法的版本,可以是2a,2b,2y
* random:用于生成随机的盐
*/
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2Y,4,new SecureRandom());
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
}
2.4.添加数据库实体类
修改生成的实体类,如下:
package com.drson.usermanagement.entity;//注意:修改包名
import lombok.Data;
@Data //添加注解
public class Employee {
//使用包装类
private Long id;
private String jobNumber;
private String name;
private Long gender;
private Long age;
private String phone;
private String address;
private String idCardNumber;
private String educationalBackground;
private String password;
private Long isAccountNonExpired;
private Long isAccountNonLocked;
private Long isCredentialsNonExpired;
private Long isEnabled;
//删除所有getter和setter
}
2.5.编写Mybatis代码
EmployeeMapper.java
package com.drson.usermanagement.mapper;
import com.drson.usermanagement.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper //添加Mybatis注解
public interface EmployeeMapper {
/**
* 根据用户名,查询用户信息
*
* @param name 用户名
*/
@Select("select * from employee where name=#{name}")
Employee getEmployeeByName(String name);
}
2.6.实现UserDetails接口
EmployeeDetails.java
package com.drson.usermanagement.service;
import com.drson.usermanagement.entity.Employee;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class EmployeeDetails implements UserDetails {
private final Employee employee;
public EmployeeDetails(Employee employee){
this.employee = employee;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
@Override
public String getPassword() {
return employee.getPassword();
}
@Override
public String getUsername() {
return employee.getName();
}
@Override
public boolean isAccountNonExpired() {
return employee.getIsAccountNonExpired() == 1;
}
@Override
public boolean isAccountNonLocked() {
return employee.getIsAccountNonLocked() == 1;
}
@Override
public boolean isCredentialsNonExpired() {
return employee.getIsCredentialsNonExpired() == 1;
}
@Override
public boolean isEnabled() {
return employee.getIsEnabled() == 1;
}
}
2.7.实现UserDetailsService接口
EmployeeDetailsService.java
package com.drson.usermanagement.service;
import com.drson.usermanagement.entity.Employee;
import com.drson.usermanagement.mapper.EmployeeMapper;
import jakarta.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class EmployeeDetailsService implements UserDetailsService {
@Resource
private EmployeeMapper employeeMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Employee employee = employeeMapper.getEmployeeByName(username);
if (Objects.isNull(employee)){
//找不到用户,抛出异常
throw new UsernameNotFoundException(username);
}
//封装用户信息
return new EmployeeDetails(employee);
}
}
2.8.编写配置文件
WebSecurityConfig.java
package com.drson.usermanagement.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.security.SecureRandom;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
//定义密码编码器
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2.9.编写控制器
TestController.java
package com.drson.usermanagement.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public String test(){
return "test";
}
}
2.10.单元测试
Chapter2Test.java
package com.drson.usermanagement;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc
public class Chapter2Test {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "test",password = "123456")
public void test() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/test"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("test"));
}
}
测试通过!