在 Spring Security 6 中集成外部数据库进行用户认证

本文旨在指导如何在 spring security 6 中通过自定义 `userdetailsservice` 实现与外部数据库的用户认证。我们将介绍如何配置 `securityfilterchain`,并结合数据访问对象(dao)从外部数据库加载用户凭据,从而替代已弃用的 `websecurityconfigureradapter` 方法,提供一套现代且安全的用户登录解决方案。

在 Spring Security 6 中,传统的 WebSecurityConfigurerAdapter 类已被弃用,取而代之的是基于组件的配置方式,主要通过定义 SecurityFilterChain Bean 来实现安全配置。对于需要从外部数据库加载用户凭据进行认证的场景,核心思路是实现 UserDetailsService 接口,并通过数据访问层(DAO 或 Repository)与数据库进行交互。

核心概念:UserDetailsService 与数据访问层

Spring Security 的认证流程依赖于 UserDetailsService 接口。该接口只有一个方法:UserDetails loadUserByUsername(String username),它负责根据用户名加载用户的详细信息,包括用户名、密码以及所拥有的权限(角色)。

为了实现从外部数据库加载用户,我们需要:

  1. 一个数据访问对象 (DAO/Repository):负责与数据库进行实际的数据交互,查询用户数据。这可以是使用 JPA Repository、MyBatis Mapper,或者像 JdbcTemplate 这样更底层的 JDBC 访问方式。
  2. 一个 UserDetailsService 的实现类:在该类中注入上述数据访问对象,并在 loadUserByUsername 方法中调用数据访问对象来获取用户数据,然后将其转换为 Spring Security 期望的 UserDetails 对象。

步骤一:配置 Spring Security 6 的 SecurityFilterChain

首先,我们需要一个 SecurityConfiguration 类来定义 SecurityFilterChain Bean,这是 Spring Security 6 的主要配置入口。在这个配置中,我们将定义授权规则和启用表单登录。Spring Security 会自动检测并使用容器中存在的 UserDetailsService 类型的 Bean 进行用户认证。

package de.gabriel.vertretungsplan.security;

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.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((requests) -> requests
                        // 定义不同角色的访问权限
                        .requestMatchers("/vertretungsplan").hasAnyRole("SCHUELER", "LEHRER", "VERWALTUNG")
                        .requestMatchers("/account").hasAnyRole("LEHRER", "VERWALTUNG")
                        .requestMatchers("/administration").hasRole("VERWALTUNG")
                        // 允许所有用户访问根路径
                        .requestMatchers("/").permitAll()
                        // 任何其他请求都需要认证
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        // 可以指定自定义的登录页面路径,例如 "/login"
                        // .loginPage("/login")
                        // 允许所有用户访问登录页面
                        .permitAll()
                );
        return http.build();
    }

    /**
     * 配置密码编码器。强烈建议在生产环境中使用强密码编码器。
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

在上述配置中,我们定义了授权规则,并启用了 formLogin。重要的是,我们还定义了一个 PasswordEncoder Bean。Spring Security 在进行密码比对时会使用这个编码器。

步骤二:实现自定义 UserDetailsService

接下来,创建一个实现 UserDetailsService 接口的类。这个类将负责从数据库中检索用户信息。

首先,定义一个简单的用户实体类 UserEntity 来映射数据库中的用户表结构。

// src/main/java/your/package/model/UserEntity.java
package your.package.model;

import java.util.List;

public class UserEntity {
    private String username;
    private String password; // 存储加密后的密码
    private List roles; // 例如 "SCHUELER", "LEHRER", "VERWALTUNG"

    public UserEntity(String username, String password, List roles) {
        this.username = username;
        this.password = password;
        this.roles = roles;
    }

    // Getters
    public String getUsername() { return username; }
    public String getPassword() { return password; }
    public List getRoles() { return roles; }

    // Setters (根据需要添加)
}

然后,实现 CustomUserDetailsService:

// src/main/java/your/package/security/CustomUserDetailsService.java
package your.package.security;

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.Service;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import your.package.model.UserEntity;
import your.package.repository.UserRepository; // 引入你的用户数据访问接口

import java.util.List;
import java.util.stream.Collectors;

@Service // 声明为一个Spring服务组件,使其可被Spring容器管理
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository; // 注入用户数据访问接口

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库获取用户实体
        UserEntity userEntity = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户未找到: " + username));

        // 2. 将用户实体中的角色转换为 Spring Security 期望的 SimpleGrantedAuthority 列表
        // 注意:Spring Security 默认期望角色以 "ROLE_" 开头,例如 "ROLE_SCHUELER"
        List authorities = userEntity.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());

        // 3. 构建并返回 Spring Security 的 UserDetails 对象
        return new org.springframework.security.core.userdetails.User(
                userEntity.getUsername(),    // 用户名
                userEntity.getPassword(),    // 数据库中存储的加密密码
                authorities                  // 用户权限列表
        );
    }
}

步骤三:创建数据访问层(DAO/Repository)

为了让 CustomUserDetailsService 能够访问数据库,我们需要一个数据访问层。这里以 JdbcTemplate 为例,因为它轻量且易于理解。

首先,定义 UserRepository 接口:

// src/main/java/your/package/repository/UserRepository.java
package your.package.repository;

import your.package.model.UserEntity;
import java.util.Optional;

public interface UserRepository {
    Optional findByUsername(String username);
}

然后,实现 JdbcUserRepository 使用 JdbcTemplate:

// src/main/java/your/package/repository/JdbcUserRepository.java
package your.pac

kage.repository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import your.package.model.UserEntity; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.List; import java.util.Optional; @Repository // 声明为一个Spring数据访问组件 public class JdbcUserRepository implements UserRepository { private final JdbcTemplate jdbcTemplate; public JdbcUserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public Optional findByUsername(String username) { // 假设数据库中有一个名为 'users' 的表,包含 'username', 'password', 'roles' 列 // 'roles' 列存储逗号分隔的角色字符串,例如 "SCHUELER,LEHRER" String sql = "SELECT username, password, roles FROM users WHERE username = ?"; try { return Optional.ofNullable(jdbcTemplate.queryForObject(sql, new UserEntityRowMapper(), username)); } catch (org.springframework.dao.EmptyResultDataAccessException e) { // 如果没有找到用户,JdbcTemplate 会抛出 EmptyResultDataAccessException return Optional.empty(); } } // 辅助类,用于将 ResultSet 的一行映射到 UserEntity 对象 private static class UserEntityRowMapper implements RowMapper { @Override public UserEntity mapRow(ResultSet rs, int rowNum) throws SQLException { String rolesString = rs.getString("roles"); List roles = Arrays.asList(rolesString.split(",")); // 将逗号分隔的字符串转换为列表 return new UserEntity( rs.getString("username"), rs.getString("password"), roles );