Spring Security 6 无密码认证实现指南

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>

安全注意事项

  1. 令牌安全

    • 使用强随机数生成令牌(SecureRandom
    • 设置合理的过期时间(5-15分钟)
    • 单次使用后立即失效
  2. 传输安全

    • 始终使用HTTPS
    • 在魔法链接中包含用户标识(防止令牌劫持)
    • 避免在URL中暴露敏感信息
  3. 防滥用机制

    • 实施速率限制(例如每分钟最多3次请求)
    • 监控异常登录模式
    • 提供用户反馈机制
  4. 用户体验

    • 清晰的错误消息
    • 加载状态指示器
    • 成功/失败反馈

最佳实践

  1. 多因素认证:对于敏感操作,结合无密码认证与其他因素(如设备信任)

  2. 会话管理

    • 使用安全的HttpOnly Cookie
    • 实施会话超时
    • 提供"记住我"功能
  3. 监控与日志

    • 记录所有认证尝试
    • 监控失败率
    • 实施异常检测
  4. 用户教育

    • 解释无密码认证的工作原理
    • 提供安全提示
    • 提供备选认证方式

Spring Security 6的无密码认证实现提供了强大的安全性和灵活性,开发者可以根据具体需求选择最适合的方案。魔法链接适合大多数Web应用,OTP适合移动应用,而WebAuthn则提供最高级别的安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值