Spring Security 6 自定义用户信息实现指南
在 Spring Security 6 中,自定义用户信息是常见需求,下面我将展示如何完全自定义用户信息存储、认证流程和用户详情对象。
完整实现方案
1. 自定义用户实体类
@Entity
@Table(name = "users")
public class CustomUser implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String email;
private String phone;
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
// 自定义字段
private String department;
private LocalDate hireDate;
private boolean mfaEnabled;
private String secretKey;
// 实现 UserDetails 接口方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// Getters and Setters
// 省略...
}
2. 自定义 UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
// 自定义方法:通过邮箱加载用户
public UserDetails loadUserByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
}
// 自定义方法:更新用户信息
@Transactional
public void updateUserDetails(String username, Consumer<CustomUser> updater) {
CustomUser user = (CustomUser) loadUserByUsername(username);
updater.accept(user);
userRepository.save(user);
}
}
3. 自定义用户仓库
public interface UserRepository extends JpaRepository<CustomUser, Long> {
Optional<CustomUser> findByUsername(String username);
Optional<CustomUser> findByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
@Query("SELECT u FROM CustomUser u WHERE u.department = :department")
List<CustomUser> findByDepartment(String department);
}
4. 自定义认证提供器
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
private final OtpService otpService; // 自定义OTP服务
public CustomAuthenticationProvider(CustomUserDetailsService uds,
PasswordEncoder pe,
OtpService os) {
this.userDetailsService = uds;
this.passwordEncoder = pe;
this.otpService = os;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
CustomUser user = (CustomUser) userDetailsService.loadUserByUsername(username);
// 1. 验证密码
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
// 2. 验证MFA(如果启用)
if (user.isMfaEnabled()) {
String otp = ((CustomWebAuthenticationDetails) authentication.getDetails()).getOtp();
if (!otpService.validateOtp(user.getSecretKey(), otp)) {
throw new BadCredentialsException("Invalid OTP");
}
}
// 3. 检查账户状态
if (!user.isEnabled()) {
throw new DisabledException("Account disabled");
}
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
// 自定义认证方法:使用邮箱登录
public Authentication authenticateByEmail(String email, String password) {
CustomUser user = (CustomUser) userDetailsService.loadUserByEmail(email);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities());
}
}
5. 自定义认证详情对象
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
private final String otp;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
this.otp = request.getParameter("otp");
}
public String getOtp() {
return otp;
}
}
6. 安全配置集成
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final CustomAuthenticationProvider customAuthProvider;
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomAuthenticationProvider cap,
CustomUserDetailsService uds) {
this.customAuthProvider = cap;
this.userDetailsService = uds;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/register", "/login", "/mfa-setup").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard")
.authenticationDetailsSource(new CustomAuthenticationDetailsSource())
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.authenticationProvider(customAuthProvider)
.userDetailsService(userDetailsService)
.exceptionHandling(exception -> exception
.accessDeniedHandler(new CustomAccessDeniedHandler())
);
return http.build();
}
@Bean
public AuthenticationDetailsSource<HttpServletRequest,
WebAuthenticationDetails>
customAuthenticationDetailsSource() {
return CustomWebAuthenticationDetails::new;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
7. 自定义访问拒绝处理器
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setStatus(HttpStatus.FORBIDDEN.value());
if (request.getRequestURI().startsWith("/api/")) {
// API返回JSON错误
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
Map<String, Object> body = Map.of(
"status", HttpStatus.FORBIDDEN.value(),
"error", "Forbidden",
"message", "Access denied",
"path", request.getRequestURI()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(body));
} else {
// Web页面重定向
response.sendRedirect("/access-denied");
}
}
}
8. 用户注册服务
@Service
public class UserRegistrationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
public UserRegistrationService(UserRepository ur,
PasswordEncoder pe,
EmailService es) {
this.userRepository = ur;
this.passwordEncoder = pe;
this.emailService = es;
}
@Transactional
public void registerUser(UserRegistrationDto registrationDto) {
if (userRepository.existsByUsername(registrationDto.getUsername())) {
throw new UsernameAlreadyExistsException(
"Username already exists: " + registrationDto.getUsername());
}
if (userRepository.existsByEmail(registrationDto.getEmail())) {
throw new EmailAlreadyExistsException(
"Email already registered: " + registrationDto.getEmail());
}
CustomUser user = new CustomUser();
user.setUsername(registrationDto.getUsername());
user.setPassword(passwordEncoder.encode(registrationDto.getPassword()));
user.setEmail(registrationDto.getEmail());
user.setPhone(registrationDto.getPhone());
user.setDepartment(registrationDto.getDepartment());
user.setHireDate(LocalDate.now());
user.setRoles(Set.of("ROLE_USER")); // 默认角色
userRepository.save(user);
// 发送欢迎邮件
emailService.sendWelcomeEmail(user);
}
// 自定义注册方法:使用部门默认角色
public void registerWithDepartmentRole(UserRegistrationDto dto, String departmentRole) {
registerUser(dto);
userDetailsService.updateUserDetails(dto.getUsername(), user -> {
user.getRoles().add("ROLE_" + departmentRole.toUpperCase());
});
}
}
9. 自定义用户控制器
@RestController
@RequestMapping("/api/users")
public class UserController {
private final CustomUserDetailsService userDetailsService;
private final UserRegistrationService registrationService;
public UserController(CustomUserDetailsService uds,
UserRegistrationService urs) {
this.userDetailsService = uds;
this.registrationService = urs;
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody @Valid UserRegistrationDto dto) {
registrationService.registerUser(dto);
return ResponseEntity.ok(Map.of("message", "User registered successfully"));
}
@GetMapping("/me")
public ResponseEntity<UserProfileDto> getCurrentUser(
@AuthenticationPrincipal CustomUser user) {
UserProfileDto profile = new UserProfileDto(
user.getUsername(),
user.getEmail(),
user.getPhone(),
user.getDepartment(),
user.getHireDate()
);
return ResponseEntity.ok(profile);
}
@PutMapping("/mfa")
public ResponseEntity<?> toggleMfa(@AuthenticationPrincipal CustomUser user) {
userDetailsService.updateUserDetails(user.getUsername(), u -> {
if (u.isMfaEnabled()) {
u.setMfaEnabled(false);
u.setSecretKey(null);
} else {
u.setMfaEnabled(true);
u.setSecretKey(SecretGenerator.generateBase32Secret());
}
});
return ResponseEntity.ok(Map.of(
"mfaEnabled", !user.isMfaEnabled(),
"message", "MFA status updated"
));
}
// DTO 类
public record UserRegistrationDto(
@NotBlank String username,
@NotBlank @Size(min = 8) String password,
@Email String email,
@Pattern(regexp = "^\\+?[0-9]{10,15}$") String phone,
String department
) {}
public record UserProfileDto(
String username,
String email,
String phone,
String department,
LocalDate hireDate
) {}
}
10. 自定义登录页面(带MFA支持)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Custom Login</title>
</head>
<body>
<div class="login-container">
<h2>Custom Login</h2>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label>Username:</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" name="password" required>
</div>
<!-- MFA 字段(仅对启用MFA的用户显示) -->
<div class="form-group" id="mfaGroup" style="display: none;">
<label>MFA Code:</label>
<input type="text" name="otp" placeholder="6-digit code">
</div>
<div class="form-group remember">
<label>
<input type="checkbox" name="remember-me"> Remember me
</label>
</div>
<button type="submit">Login</button>
</form>
<div class="alternative-login">
<a href="/login/email">Login with Email</a>
</div>
</div>
<script>
// 动态显示MFA字段
document.querySelector('input[name="username"]').addEventListener('blur', async function() {
const username = this.value;
if (username) {
const response = await fetch(`/api/users/mfa-status?username=${username}`);
if (response.ok) {
const data = await response.json();
document.getElementById('mfaGroup').style.display =
data.mfaEnabled ? 'block' : 'none';
}
}
});
</script>
</body>
</html>
关键功能说明
1. 自定义用户实体
- 实现
UserDetails
接口,集成 Spring Security 认证体系 - 添加自定义字段:部门、入职日期、MFA状态等
- 使用 JPA 注解映射数据库关系
2. 自定义认证流程
-
自定义认证提供器:
- 实现
AuthenticationProvider
接口 - 添加多因素认证(MFA)支持
- 扩展邮箱认证方式
- 实现
-
自定义认证详情:
- 扩展
WebAuthenticationDetails
添加OTP字段 - 在安全配置中注册自定义详情源
- 扩展
3. 高级用户管理功能
-
用户注册服务:
- 用户名/邮箱唯一性验证
- 密码加密存储
- 发送欢迎邮件
-
用户详情服务扩展:
- 支持邮箱加载用户
- 提供用户信息更新方法
- 部门用户批量管理
4. 安全集成
-
安全配置:
- 注册自定义认证提供器
- 使用自定义用户详情服务
- 配置认证详情源
- 设置自定义访问拒绝处理器
-
方法级安全:
- 使用
@EnableMethodSecurity
开启 - 在服务层使用
@PreAuthorize
注解
- 使用
最佳实践建议
1. 密码安全
@Bean
public PasswordEncoder passwordEncoder() {
int strength = 12; // 越高越安全,但性能开销越大
return new BCryptPasswordEncoder(strength);
}
2. 敏感数据处理
@Entity
public class CustomUser implements UserDetails {
@Column(nullable = false)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
@Column
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String secretKey; // MFA密钥
}
3. 审计跟踪
@EntityListeners(AuditingEntityListener.class)
public class CustomUser implements UserDetails {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
}
4. 安全事件监听
@Component
public class CustomSecurityEventListener {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
CustomUser user = (CustomUser) event.getAuthentication().getPrincipal();
log.info("Login successful: {}", user.getUsername());
// 更新登录时间、IP等信息
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
String username = (String) event.getAuthentication().getPrincipal();
log.warn("Login failed for user: {}", username);
// 实现登录失败锁定策略
}
}
测试自定义用户系统
1. 测试用户注册
@SpringBootTest
class UserRegistrationTest {
@Autowired
private UserRegistrationService registrationService;
@Test
void testUserRegistration() {
UserRegistrationDto dto = new UserRegistrationDto(
"testuser", "P@ssw0rd123", "test@example.com", "+1234567890", "IT");
registrationService.registerUser(dto);
CustomUser user = (CustomUser) userDetailsService.loadUserByUsername("testuser");
assertNotNull(user);
assertEquals("test@example.com", user.getEmail());
assertTrue(user.getRoles().contains("ROLE_USER"));
}
}
2. 测试认证流程
@SpringBootTest
class AuthenticationTest {
@Autowired
private CustomAuthenticationProvider authProvider;
@Test
void testAuthentication() {
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken("testuser", "P@ssw0rd123");
Authentication auth = authProvider.authenticate(authRequest);
assertTrue(auth.isAuthenticated());
CustomUser user = (CustomUser) auth.getPrincipal();
assertEquals("IT", user.getDepartment());
}
}
总结
通过以上实现,我们创建了一个完全自定义的用户系统,具有以下特点:
-
灵活的用户模型:
- 扩展标准用户详情
- 添加业务相关字段
- 支持多因素认证
-
定制认证流程:
- 自定义认证提供器
- 支持多种登录方式
- 集成MFA验证
-
高级用户管理:
- 用户注册服务
- 部门角色分配
- 用户信息更新API
-
安全集成:
- 自定义安全配置
- 细粒度访问控制
- 审计日志记录
这种自定义用户系统适合需要深度集成企业用户管理需求的应用场景,可以轻松扩展以适应各种业务需求。