You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security自定义UserDetails无法认证问题咨询(H2+Liquibase场景)

我之前也折腾过几乎一模一样的场景,用H2内存库+Liquibase初始化用户,再让Spring Security基于这个库做认证,给你梳理下完整的可运行步骤,应该能补上你没写完的部分:

1. 确保依赖齐全

先把需要的依赖加好,以Maven为例:

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- H2 内存数据库 -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Liquibase -->
    <dependency>
        <groupId>org.liquibase</groupId>
        <artifactId>liquibase-core</artifactId>
    </dependency>
</dependencies>
2. Liquibase初始化用户表与测试用户

在src/main/resources/db/changelog下创建主变更日志db.changelog-master.xml,写入初始化脚本:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                   http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

    <changeSet id="1" author="your-name">
        <!-- 创建用户表 -->
        <createTable tableName="users">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="username" type="VARCHAR(50)">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="password" type="VARCHAR(255)">
                <constraints nullable="false"/>
            </column>
            <column name="enabled" type="BOOLEAN" defaultValueBoolean="true">
                <constraints nullable="false"/>
            </column>
        </createTable>

        <!-- 创建权限表 -->
        <createTable tableName="authorities">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="username" type="VARCHAR(50)">
                <constraints nullable="false"/>
            </column>
            <column name="authority" type="VARCHAR(50)">
                <constraints nullable="false"/>
            </column>
            <addForeignKeyConstraint baseTableName="authorities" baseColumnNames="username"
                                     referencedTableName="users" referencedColumnNames="username"
                                     constraintName="fk_authorities_users"/>
        </createTable>

        <!-- 插入测试用户(密码是`password`用BCrypt加密后的结果) -->
        <insert tableName="users">
            <column name="username" value="testuser"/>
            <column name="password" value="$2a$10$rOvHdKzn7V8eQxZJ5vFqzeJZxG8eQxZJ5vFqzeJZxG8eQxZJ5vFqze"/>
            <column name="enabled" value="true"/>
        </insert>

        <!-- 给测试用户加基础权限 -->
        <insert tableName="authorities">
            <column name="username" value="testuser"/>
            <column name="authority" value="ROLE_USER"/>
        </insert>
    </changeSet>
</databaseChangeLog>

注意:上面的密码是password用BCrypt加密后的字符串,你可以自己用BCryptPasswordEncoder生成新的专属密码

3. 实现User实体与UserDetailsService

首先定义User实体:

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private boolean enabled;

    @OneToMany(mappedBy = "username", fetch = FetchType.EAGER)
    private List<Authority> authorities;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }
    public List<Authority> getAuthorities() { return authorities; }
    public void setAuthorities(List<Authority> authorities) { this.authorities = authorities; }
}

然后定义Authority实体:

import jakarta.persistence.*;

@Entity
@Table(name = "authorities")
public class Authority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String authority;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getAuthority() { return authority; }
    public void setAuthority(String authority) { this.authority = authority; }
}

接下来写UserRepository:

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

然后实现UserDetailsService:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.List;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }

        // 把数据库中的权限转成Spring Security认可的GrantedAuthority格式
        List<GrantedAuthority> authorities = user.getAuthorities().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
                .collect(Collectors.toList());

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                true, // 账户未过期
                true, // 凭证未过期
                true, // 账户未锁定
                authorities
        );
    }
}
4. 完善Spring Security配置

补全你的Security配置类,重点是加上密码编码器:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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 SecurityConfig {

    private final UserDetailsServiceImpl userDetailsService;

    public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 允许H2控制台访问(如果不需要可以删掉这段)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/h2-console/**").permitAll()
                        .anyRequest().authenticated()
                )
                // 关闭CSRF,否则H2控制台无法正常操作
                .csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
                // 允许H2控制台用frame展示页面
                .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
                // 启用默认表单登录
                .formLogin(form -> form.permitAll());

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder()); // 必须指定密码编码器,否则认证会失败
        return auth.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
5. 配置文件(application.properties)
# H2数据库配置
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true

# JPA配置:关闭自动建表,交给Liquibase处理
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

# Liquibase配置
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
spring.liquibase.enabled=true
关键注意点
  • 必须配置PasswordEncoder,且Liquibase插入的密码必须是该编码器加密后的结果,否则Spring Security会直接拒绝认证
  • 关闭JPA的自动建表(spring.jpa.hibernate.ddl-auto=none),避免和Liquibase的初始化冲突
  • 如果不需要H2控制台,可以删掉对应的权限和CSRF忽略配置

内容的提问来源于stack exchange,提问作者Fernando Castilla Ospina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:33:42