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

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

Dec 04, 2024 pm 05:11 PM

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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

2025年的前4个JavaScript框架:React,Angular,Vue,Svelte 2025年的前4个JavaScript框架:React,Angular,Vue,Svelte Mar 07, 2025 pm 06:09 PM

2025年的前4个JavaScript框架:React,Angular,Vue,Svelte

如何使用咖啡因或Guava Cache等库在Java应用程序中实现多层缓存? 如何使用咖啡因或Guava Cache等库在Java应用程序中实现多层缓存? Mar 17, 2025 pm 05:44 PM

如何使用咖啡因或Guava Cache等库在Java应用程序中实现多层缓存?

Node.js 20:关键性能提升和新功能 Node.js 20:关键性能提升和新功能 Mar 07, 2025 pm 06:12 PM

Node.js 20:关键性能提升和新功能

Java的类负载机制如何起作用,包括不同的类载荷及其委托模型? Java的类负载机制如何起作用,包括不同的类载荷及其委托模型? Mar 17, 2025 pm 05:35 PM

Java的类负载机制如何起作用,包括不同的类载荷及其委托模型?

冰山:数据湖桌的未来 冰山:数据湖桌的未来 Mar 07, 2025 pm 06:31 PM

冰山:数据湖桌的未来

Spring Boot Snakeyaml 2.0 CVE-2022-1471问题已修复 Spring Boot Snakeyaml 2.0 CVE-2022-1471问题已修复 Mar 07, 2025 pm 05:52 PM

Spring Boot Snakeyaml 2.0 CVE-2022-1471问题已修复

如何将JPA(Java持久性API)用于具有高级功能(例如缓存和懒惰加载)的对象相关映射? 如何将JPA(Java持久性API)用于具有高级功能(例如缓存和懒惰加载)的对象相关映射? Mar 17, 2025 pm 05:43 PM

如何将JPA(Java持久性API)用于具有高级功能(例如缓存和懒惰加载)的对象相关映射?

如何将Maven或Gradle用于高级Java项目管理,构建自动化和依赖性解决方案? 如何将Maven或Gradle用于高级Java项目管理,构建自动化和依赖性解决方案? Mar 17, 2025 pm 05:46 PM

如何将Maven或Gradle用于高级Java项目管理,构建自动化和依赖性解决方案?

See all articles