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
相关产品推荐
相关产品推荐

