首页 > Java > java教程 > 使用 Spring Security 实现一次性令牌身份验证

使用 Spring Security 实现一次性令牌身份验证

DDD
发布: 2024-12-04 17:11:11
原创
672 人浏览过

Implementing One-Time Token Authentication with Spring Security

在当今的数字环境中,提供安全且用户友好的身份验证方法至关重要。一种越来越流行的方法是一次性令牌 (OTT) 身份验证,通常以通过电子邮件发送的“魔术链接”的形式实现。 Spring Security 6.4.0 为 OTT 身份验证提供强大的内置支持,包括即用型实现。在这份综合指南中,我们将探讨如何使用内置解决方案和自定义实现来实现安全的 OTT 身份验证。

了解一次性令牌与一次性密码

在深入实施之前,了解一次性令牌 (OTT) 与一次性密码 (OTP) 的不同非常重要。虽然 OTP 系统通常需要初始设置并依赖外部工具来生成密码,但从用户角度来看,OTT 系统更简单 - 它们会收到可用于身份验证的唯一令牌(通常通过电子邮件)。

主要区别包括:

  • OTT 不需要初始用户设置
  • 令牌由您的应用程序生成和交付
  • 每个令牌通常只能使用一次,并在设定时间后过期

可用的内置实现

Spring Security 提供了 OneTimeTokenService 的两种实现:

  1. InMemoryOneTimeTokenService:

    • 将令牌存储在内存中
    • 非常适合开发和测试
    • 不适合生产或集群环境
    • 应用程序重新启动时令牌会丢失
  2. JdbcOneTimeTokenService:

    • 将令牌存储在数据库中
    • 适合生产使用
    • 在集群环境中工作
    • 具有自动清理功能的持久存储

使用 InMemoryOneTimeTokenService

以下是如何实现更简单的内存解决方案:

@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();
    }
}
登录后复制
登录后复制

使用 JdbcOneTimeTokenService

对于生产环境,使用 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
);
登录后复制

定制实施

为了更好地控制令牌生成和验证过程,您可以创建自定义实现:

1. Token实体和存储库

@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);
}
登录后复制

2. 自定义令牌服务

@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);
    }
}
登录后复制

自定义 URL 和页面

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 身份验证时:

  1. 选择正确的实施

    • 使用 JdbcOneTimeTokenService 或自定义实现进行生产
    • InMemoryOneTimeTokenService 只能用于开发/测试
  2. 配置电子邮件传送

@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();
    }
}
登录后复制
登录后复制
  1. 安全最佳实践
    • 设置适当的令牌过期时间(建议 15 分钟)
    • 对代币生成实施速率限制
    • 对所有端点使用 HTTPS
    • 监控失败的身份验证尝试
    • 确保代币是一次性的,使用后立即失效
    • 实现过期令牌的自动清理
    • 使用安全随机令牌生成来防止猜测

它是如何运作的

  1. 用户通过提交电子邮件地址请求令牌
  2. 系统生成安全令牌并通过电子邮件发送魔法链接
  3. 用户点击链接并被重定向到令牌提交页面
  4. 系统验证令牌并验证用户(如果有效)

结论

Spring Security 的 OTT 支持为实现安全、用户友好的身份验证提供了坚实的基础。无论您选择内置实现还是创建自定义解决方案,您都可以为用户提供无密码登录选项,同时保持高安全标准。

实施 OTT 身份验证时,请记住:

  • 选择适合您环境的实施
  • 实施安全令牌交付
  • 配置正确的令牌过期时间
  • 遵循安全最佳实践
  • 创建用户友好的错误处理和重定向
  • 实施适当的电子邮件模板以获得专业外观

通过遵循本指南,您可以实现安全且用户友好的 OTT 身份验证系统,满足您的应用程序的需求,同时利用 Spring Security 强大的安全功能。

参考:https://docs.spring.io/spring-security/reference/servlet/authentication/onetimetoken.html

以上是使用 Spring Security 实现一次性令牌身份验证的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板