Skip to content

基于数据库的用户认证

1. 脚本准备

sql
-- 创建数据库
CREATE DATABASE `security_demo`;
USE `security_demo`;

-- 创建用户表
CREATE TABLE `user_info`(
	`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`username` VARCHAR(50) DEFAULT NULL ,
	`password` VARCHAR(500) DEFAULT NULL,
	`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user_info`(`username`); 

-- 插入用户数据(密码是 "abc" )
INSERT INTO `user_info` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);

2. 引入依赖

xml
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.4</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

3. 配置数据源

yml
datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://192.168.101.102:3306/spring_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
  username: root
  password: 123456

4. 基本代码

java
public class UserInfo {

    private String username;

    private String password;

    private boolean enabled;

    // getter/setter
}
java
@Mapper
public interface UserMapper {

    @Select("SELECT * FROM user_info WHERE username = #{username}")
    UserInfo findByUsername(String username);

    @Select("SELECT * FROM user_info")
    List<UserInfo> listAll();
}
java
public interface UserService {
    UserInfo findByUsername(String username);

    List<UserInfo> listAll();
}
java
@Service
public class UserServiceImpl implements UserService {
    @Resource
    UserMapper userMapper;

    @Override
    public UserInfo findByUsername(String username) {
        return userMapper.findByUsername(username);
    }

    @Override
    public List<UserInfo> listAll() {
        return userMapper.listAll();
    }
}
java
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserService userService;

    @GetMapping("/list")
    public List<UserInfo> listAll() {
        return userService.listAll();
    }
}

运行测试, 保障基本功能完整:
Alt text

5. 编写用户管理类

编写实现UserDetailsManager接口的类DatabaseUserDetailsManager, 我们可以看到UserDetailsManager还有一个子类JdbcUserDetailsManager,为何不直接使用JdbcUserDetailsManager类呢:
Alt text 可以看到JdbcUserDetailsManager虽然支持数据库来认证用户,但基于的是传统的jdbc而不是Mybatis, 而且里面充斥大量sql, 需要提前建好这些简易的表才能使用,可用性很低,这就需要我们按照我们的项目来定制我们的数据库用户管理类。

java
public class DatabaseUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    protected final Log logger = LogFactory.getLog(getClass());

    @Resource
    UserService userService;

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }

    @Override
    public void createUser(UserDetails user) {
        
    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

    // SpringSecurity自动使用loadUserByUsername方法从数据库中获取User对象
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = userService.findByUsername(username);
        if(userInfo != null){
            Collection<? extends GrantedAuthority> authorities = new HashSet<>();
            return User
                    .withUsername(userInfo.getUsername())
                    .password(userInfo.getPassword())
                    .disabled(!userInfo.isEnabled())
                    .accountExpired(false)
                    .credentialsExpired(false)
                    .accountLocked(false)
                    .authorities(authorities)
                    .build();
        }
        throw new UsernameNotFoundException("用户名不存在: " + username );

    }
}

6. 调整WebSecurityConfig

java
@Configuration
public class WebSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        DatabaseUserDetailsManager manager = new DatabaseUserDetailsManager();
        return manager;
    }
}

程序重新运行,输入用户名密码admin/password:
Alt text

7. 基于数据库的用户认证流程

  1. 程序启动时:
    • 创建DatabaseUserDetailsManager类,实现接口 UserDetailsManager, UserDetailsPasswordService
    • 在应用程序中初始化这个类的对象
  2. 校验用户时:
    • SpringSecurity自动使用DatabaseUserDetailsManagerloadUserByUsername方法从数据库中获取User对象
    • UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证

8. 添加用户功能实现

8.1 添加用户代码

在DatabaseUserDetailsManager中,提供了createUser()方法,需要我们手动实现:

java
@Component
public class DatabaseUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    protected final Log logger = LogFactory.getLog(getClass());

    @Resource
    UserMapper userMapper;

    @Override
    public void createUser(UserDetails user) {
        userMapper.insertOne(user);
    }
    // 中间无关代码省略
}

DatabaseUserDetailsManager加上@Component注解后,方便在UserService中依赖注入,同时不需要在WebSecurityConfig进行注册(因为Spring容器已经管理了DatabaseUserDetailsManager对象):

java
@Configuration
public class WebSecurityConfig {

    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.formLogin(withDefaults());
//        http.httpBasic(withDefaults());
        // api接口测试时关闭csrf校验
        http.csrf((csrf) -> csrf.disable());
        return http.build();
    }
}

登录功能只使用表单即可,不使用Basic弹窗登录,同时为了方便post方式接口测试,暂时关闭csrf。
其他代码实现:

java
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserService userService;

    @GetMapping("/list")
    public List<UserInfo> listAll() {
        return userService.listAll();
    }

    @PostMapping("/add")
    public String add(String name, String password) {
        userService.add(name, password, true);
        return "success";
    }
}
java
@Service
public class UserServiceImpl implements UserService {
    @Resource
    UserMapper userMapper;

    @Resource
    DatabaseUserDetailsManager userDetailsManager;

    @Override
    public List<UserInfo> listAll() {
        return userMapper.listAll();
    }

    @Transactional
    @Override
    public void add(String name, String password, boolean enabled) {
        UserDetails user = User
                .withDefaultPasswordEncoder()
                .username(name)
                .password(password)
                .disabled(!enabled)
                .authorities(new HashSet<GrantedAuthority>())
                .build();
        userDetailsManager.createUser(user);
    }
}
java
@Mapper
public interface UserMapper {

    @Insert("insert into user_info (username, password, enabled) values (#{username}, #{password}, #{enabled})")
    int insertOne(UserDetails user);
}

8.2 代码测试

添加依赖:

xml
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

**Swagger测试地址:**http://localhost:8080/demo/doc.htmlAlt text 运行测试结果:
Alt text