在当今的数字环境中,提供安全且用户友好的身份验证方法至关重要。一种越来越流行的方法是一次性令牌 (OTT) 身份验证,通常以通过电子邮件发送的“魔术链接”的形式实现。 Spring Security 6.4.0 为 OTT 身份验证提供强大的内置支持,包括即用型实现。在这份综合指南中,我们将探讨如何使用内置解决方案和自定义实现来实现安全的 OTT 身份验证。
在深入实施之前,了解一次性令牌 (OTT) 与一次性密码 (OTP) 的不同非常重要。虽然 OTP 系统通常需要初始设置并依赖外部工具来生成密码,但从用户角度来看,OTT 系统更简单 - 它们会收到可用于身份验证的唯一令牌(通常通过电子邮件)。
主要区别包括:
Spring Security 提供了 OneTimeTokenService 的两种实现:
InMemoryOneTimeTokenService:
JdbcOneTimeTokenService:
以下是如何实现更简单的内存解决方案:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); // Uses InMemoryOneTimeTokenService by default return http.build(); } }
对于生产环境,使用 JDBC 实现:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired JdbcTemplate jdbcTemplate; @Bean public OneTimeTokenService oneTimeTokenService() { return new JdbcOneTimeTokenService(jdbcTemplate); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); return http.build(); } }
JdbcOneTimeTokenService 所需的表结构:
CREATE TABLE one_time_tokens ( token_value VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL, issued_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, used BOOLEAN NOT NULL );
为了更好地控制令牌生成和验证过程,您可以创建自定义实现:
@Entity @Table(name = "one_time_tokens") public class OneTimeToken { @Id @GeneratedValue private Long id; private String tokenValue; private String username; private LocalDateTime createdAt; private LocalDateTime expiresAt; private boolean used; // Getters and setters omitted for brevity } @Repository public interface OneTimeTokenRepository extends JpaRepository<OneTimeToken, Long> { Optional<OneTimeToken> findByTokenValueAndUsedFalse(String tokenValue); void deleteByExpiresAtBefore(LocalDateTime dateTime); }
@Service @Transactional public class PersistentOneTimeTokenService implements OneTimeTokenService { private static final int TOKEN_VALIDITY_MINUTES = 15; @Autowired private OneTimeTokenRepository tokenRepository; @Override public OneTimeToken generate(GenerateOneTimeTokenRequest request) { String tokenValue = UUID.randomUUID().toString(); LocalDateTime now = LocalDateTime.now(); OneTimeToken token = new OneTimeToken(); token.setTokenValue(tokenValue); token.setUsername(request.getUsername()); token.setCreatedAt(now); token.setExpiresAt(now.plusMinutes(TOKEN_VALIDITY_MINUTES)); token.setUsed(false); return return new DefaultOneTimeToken(token.getTokenValue(),token.getUsername(), Instant.now()); } @Override public Authentication consume(ConsumeOneTimeTokenRequest request) { OneTimeToken token = tokenRepository.findByTokenValueAndUsedFalse(request.getTokenValue()) .orElseThrow(() -> new BadCredentialsException("Invalid or expired token")); if (token.getExpiresAt().isBefore(LocalDateTime.now())) { throw new BadCredentialsException("Token has expired"); } token.setUsed(true); tokenRepository.save(token); UserDetails userDetails = loadUserByUsername(token.getUsername()); return new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); } }
Spring Security 不处理令牌传递,因此您需要实现它:
@Component public class EmailMagicLinkHandler implements OneTimeTokenGenerationSuccessHandler { @Autowired private JavaMailSender mailSender; private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/check-email"); @Override public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken token) throws IOException, ServletException { String magicLink = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replacePath(request.getContextPath()) .replaceQuery(null) .fragment(null) .path("/login/ott") .queryParam("token", token.getTokenValue()) .toUriString(); SimpleMailMessage message = new SimpleMailMessage(); message.setTo(getUserEmail(token.getUsername())); message.setSubject("Your Sign-in Link"); message.setText("Click here to sign in: " + magicLink); mailSender.send(message); redirectHandler.handle(request, response, token); } }
Spring Security 提供了多种自定义选项:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); // Uses InMemoryOneTimeTokenService by default return http.build(); } }
在生产中部署 OTT 身份验证时:
选择正确的实施
配置电子邮件传送
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired JdbcTemplate jdbcTemplate; @Bean public OneTimeTokenService oneTimeTokenService() { return new JdbcOneTimeTokenService(jdbcTemplate); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); return http.build(); } }
Spring Security 的 OTT 支持为实现安全、用户友好的身份验证提供了坚实的基础。无论您选择内置实现还是创建自定义解决方案,您都可以为用户提供无密码登录选项,同时保持高安全标准。
实施 OTT 身份验证时,请记住:
通过遵循本指南,您可以实现安全且用户友好的 OTT 身份验证系统,满足您的应用程序的需求,同时利用 Spring Security 强大的安全功能。
参考:https://docs.spring.io/spring-security/reference/servlet/authentication/onetimetoken.html
以上是使用 Spring Security 实现一次性令牌身份验证的详细内容。更多信息请关注PHP中文网其他相关文章!