Spring Security 6 无密码认证实现指南
Spring Security 6 提供了强大的无密码认证支持,下面我将展示几种常见的无密码认证实现方案,包括魔法链接(Magic Link)、一次性密码(OTP)和WebAuthn。
核心实现方案
1. 魔法链接(Magic Link)认证
用户输入邮箱后,系统发送包含认证链接的邮件,用户点击链接完成登录。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/login/magic", "/auth/link").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
}
@Service
public class MagicLinkService {
@Autowired
private JavaMailSender mailSender;
@Autowired
private PasswordlessTokenRepository tokenRepository;
public void sendMagicLink(String email) {
// 生成唯一令牌
String token = UUID.randomUUID().toString();
// 保存令牌到数据库(设置10分钟有效期)
PasswordlessToken passwordlessToken = new PasswordlessToken();
passwordlessToken.setToken(token);
passwordlessToken.setEmail(email);
passwordlessToken.setExpiryDate(Instant.now().plus(10, ChronoUnit.MINUTES));
tokenRepository.save(passwordlessToken);
// 发送邮件
String link = "http://yourdomain.com/auth/link?token=" + token;
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("Your Magic Login Link");
message.setText("Click the link to login: " + link);
mailSender.send(message);
}
public Authentication authenticateToken(String token) {
PasswordlessToken passwordlessToken = tokenRepository.findByToken(token)
.orElseThrow(() -> new BadCredentialsException("Invalid token"));
// 验证令牌是否过期
if (passwordlessToken.getExpiryDate().isBefore(Instant.now())) {
throw new BadCredentialsException("Token expired");
}
// 加载用户
UserDetails userDetails = userDetailsService.loadUserByUsername(passwordlessToken.getEmail());
// 创建认证对象
return new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
}
}
@RestController
public class AuthController {
@Autowired
private MagicLinkService magicLinkService;
@PostMapping("/login/magic")
public ResponseEntity<String> requestMagicLink(@RequestParam String email) {
magicLinkService.sendMagicLink(email);
return ResponseEntity.ok("Magic link sent to your email");
}
@GetMapping("/auth/link")
public String verifyMagicLink(@RequestParam String token) {
Authentication authentication = magicLinkService.authenticateToken(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/dashboard";
}
}
2. 一次性密码(OTP)认证
用户输入邮箱后,系统发送一次性密码到用户手机或邮箱。
@Service
public class OtpService {
@Autowired
private SmsService smsService; // 或 EmailService
@Autowired
private OtpRepository otpRepository;
public void sendOtp(String email, String phone) {
// 生成6位数字OTP
String otp = String.format("%06d", new Random().nextInt(999999));
// 保存OTP到数据库(设置5分钟有效期)
OneTimePassword otpEntity = new OneTimePassword();
otpEntity.setOtp(otp);
otpEntity.setEmail(email);
otpEntity.setExpiryDate(Instant.now().plus(5, ChronoUnit.MINUTES));
otpRepository.save(otpEntity);
// 发送OTP
smsService.sendSms(phone, "Your OTP code: " + otp);
}
public Authentication authenticateOtp(String email, String otp) {
OneTimePassword otpEntity = otpRepository.findByEmailAndOtp(email, otp)
.orElseThrow(() -> new BadCredentialsException("Invalid OTP"));
// 验证OTP是否过期
if (otpEntity.getExpiryDate().isBefore(Instant.now())) {
throw new BadCredentialsException("OTP expired");
}
// 加载用户
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
// 创建认证对象
return new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
}
}
@RestController
public class OtpController {
@Autowired
private OtpService otpService;
@PostMapping("/login/otp/request")
public ResponseEntity<String> requestOtp(@RequestParam String email,
@RequestParam String phone) {
otpService.sendOtp(email, phone);
return ResponseEntity.ok("OTP sent to your phone");
}
@PostMapping("/login/otp/verify")
public ResponseEntity<String> verifyOtp(@RequestParam String email,
@RequestParam String otp) {
Authentication authentication = otpService.authenticateOtp(email, otp);
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok("Authentication successful");
}
}
3. WebAuthn(基于FIDO2标准)
使用硬件安全密钥或生物识别技术进行认证。
@Configuration
@EnableWebSecurity
public class WebAuthnConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/webauthn/**", "/login", "/register").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
@Bean
public WebAuthnRegistrationService webAuthnRegistrationService() {
return new WebAuthnRegistrationService();
}
@Bean
public WebAuthnAuthenticationService webAuthnAuthenticationService() {
return new WebAuthnAuthenticationService();
}
}
@RestController
@RequestMapping("/webauthn")
public class WebAuthnController {
@Autowired
private WebAuthnRegistrationService registrationService;
@Autowired
private WebAuthnAuthenticationService authenticationService;
@PostMapping("/register/start")
public PublicKeyCredentialCreationOptions startRegistration(@RequestParam String username) {
return registrationService.startRegistration(username);
}
@PostMapping("/register/finish")
public ResponseEntity<String> finishRegistration(
@RequestParam String username,
@RequestBody AuthenticatorAttestationResponse response) {
registrationService.finishRegistration(username, response);
return ResponseEntity.ok("Registration successful");
}
@PostMapping("/authenticate/start")
public PublicKeyCredentialRequestOptions startAuthentication(@RequestParam String username) {
return authenticationService.startAuthentication(username);
}
@PostMapping("/authenticate/finish")
public ResponseEntity<String> finishAuthentication(
@RequestParam String username,
@RequestBody AuthenticatorAssertionResponse response) {
Authentication authentication = authenticationService.finishAuthentication(username, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok("Authentication successful");
}
}
完整实现示例:Magic Link认证
实体类
@Entity
public class PasswordlessToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String token;
private String email;
private Instant expiryDate;
// Getters and setters
}
仓库接口
public interface PasswordlessTokenRepository extends JpaRepository<PasswordlessToken, Long> {
Optional<PasswordlessToken> findByToken(String token);
}
安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringRequestMatchers("/auth/link", "/login/magic"))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/login/magic", "/auth/link").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 仅用于演示
}
}
前端登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Passwordless Login</title>
<style>
.container { max-width: 400px; margin: 50px auto; }
.form-group { margin-bottom: 15px; }
.tab { display: none; }
.active { display: block; }
</style>
</head>
<body>
<div class="container">
<div id="emailTab" class="tab active">
<h2>Passwordless Login</h2>
<form id="emailForm" th:action="@{/login/magic}" method="post">
<div class="form-group">
<label>Email:</label>
<input type="email" name="email" required>
</div>
<button type="submit">Send Magic Link</button>
</form>
</div>
<div id="successTab" class="tab">
<h2>Check Your Email</h2>
<p>We've sent a magic link to your email. Click it to log in.</p>
</div>
</div>
<script>
document.getElementById('emailForm').addEventListener('submit', function(e) {
e.preventDefault();
fetch(this.action, {
method: 'POST',
body: new FormData(this)
}).then(() => {
document.getElementById('emailTab').classList.remove('active');
document.getElementById('successTab').classList.add('active');
});
});
</script>
</body>
</html>
安全注意事项
-
令牌安全:
- 使用强随机数生成令牌(
SecureRandom
) - 设置合理的过期时间(5-15分钟)
- 单次使用后立即失效
- 使用强随机数生成令牌(
-
传输安全:
- 始终使用HTTPS
- 在魔法链接中包含用户标识(防止令牌劫持)
- 避免在URL中暴露敏感信息
-
防滥用机制:
- 实施速率限制(例如每分钟最多3次请求)
- 监控异常登录模式
- 提供用户反馈机制
-
用户体验:
- 清晰的错误消息
- 加载状态指示器
- 成功/失败反馈
最佳实践
-
多因素认证:对于敏感操作,结合无密码认证与其他因素(如设备信任)
-
会话管理:
- 使用安全的HttpOnly Cookie
- 实施会话超时
- 提供"记住我"功能
-
监控与日志:
- 记录所有认证尝试
- 监控失败率
- 实施异常检测
-
用户教育:
- 解释无密码认证的工作原理
- 提供安全提示
- 提供备选认证方式
Spring Security 6的无密码认证实现提供了强大的安全性和灵活性,开发者可以根据具体需求选择最适合的方案。魔法链接适合大多数Web应用,OTP适合移动应用,而WebAuthn则提供最高级别的安全性。