You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Angular与Spring Boot的用户邮箱/手机号OTP验证方案问询

Angular + Spring Boot 注册邮箱/手机号OTP验证最优实现方案

核心设计思路

  • 缓存存储OTP:用Redis存储一次性密码,设置5分钟过期时间,避免占用数据库资源,自动失效无需手动清理
  • 异步发送消息:邮件/短信发送采用异步处理,避免阻塞注册请求,提升用户体验
  • 独立校验机制:邮箱和手机号的OTP分开生成、存储与校验,互不影响
  • 防刷限制:限制同一邮箱/手机号的OTP发送频率(如1分钟最多3次),防止恶意攻击

后端(Spring Boot)具体实现

1. 依赖准备

在pom.xml中添加必要依赖:

<!-- Redis缓存 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 邮件发送 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 异步支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:Spring Retry处理发送失败重试 -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

2. OTP生成工具

实现随机6位数字OTP生成:

import java.security.SecureRandom;

@Component
public class OtpGenerator {
    public String generate6DigitOtp() {
        SecureRandom random = new SecureRandom();
        // 生成100000-999999之间的随机数
        int otp = 100000 + random.nextInt(900000);
        return String.valueOf(otp);
    }
}

3. Redis缓存操作

封装OTP的存储与校验逻辑:

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class OtpCacheService {
    private final StringRedisTemplate redisTemplate;
    private static final int OTP_EXPIRE_MINUTES = 5;

    public OtpCacheService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 存储邮箱OTP
    public void saveEmailOtp(String email, String otp) {
        String key = String.format("otp:email:%s", email);
        redisTemplate.opsForValue().set(key, otp, OTP_EXPIRE_MINUTES, TimeUnit.MINUTES);
    }

    // 存储手机号OTP
    public void savePhoneOtp(String phone, String otp) {
        String key = String.format("otp:phone:%s", phone);
        redisTemplate.opsForValue().set(key, otp, OTP_EXPIRE_MINUTES, TimeUnit.MINUTES);
    }

    // 校验邮箱OTP,校验成功后删除缓存防止重复使用
    public boolean validateEmailOtp(String email, String inputOtp) {
        String key = String.format("otp:email:%s", email);
        String storedOtp = redisTemplate.opsForValue().get(key);
        if (inputOtp.equals(storedOtp)) {
            redisTemplate.delete(key);
            return true;
        }
        return false;
    }

    // 校验手机号OTP
    public boolean validatePhoneOtp(String phone, String inputOtp) {
        String key = String.format("otp:phone:%s", phone);
        String storedOtp = redisTemplate.opsForValue().get(key);
        if (inputOtp.equals(storedOtp)) {
            redisTemplate.delete(key);
            return true;
        }
        return false;
    }
}

4. 消息发送服务

实现异步邮件和短信发送(短信需对接第三方服务如阿里云/腾讯云):

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MessageService {
    private final JavaMailSender mailSender;
    // 注入短信服务客户端(如阿里云SmsClient)

    public MessageService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    @Async
    public void sendEmailOtp(String toEmail, String otp) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject("注册验证码");
        message.setText(String.format("你的注册验证码是:%s,5分钟内有效,请尽快完成验证。", otp));
        mailSender.send(message);
    }

    @Async
    public void sendSmsOtp(String phone, String otp) {
        // 调用第三方短信服务API发送验证码
        // 示例:aliyunSmsClient.sendSms(phone, "你的注册验证码是:" + otp);
    }
}

5. 注册接口实现

分两步接口:初始化注册(发送OTP)、验证OTP并创建用户:

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final OtpGenerator otpGenerator;
    private final OtpCacheService otpCacheService;
    private final MessageService messageService;
    private final UserRepository userRepository; // 假设已定义用户Repository

    public AuthController(OtpGenerator otpGenerator, OtpCacheService otpCacheService,
                          MessageService messageService, UserRepository userRepository) {
        this.otpGenerator = otpGenerator;
        this.otpCacheService = otpCacheService;
        this.messageService = messageService;
        this.userRepository = userRepository;
    }

    // 第一步:提交注册信息,发送OTP
    @PostMapping("/register/init")
    public ResponseEntity<String> initRegister(@RequestBody RegisterInitRequest request) {
        // 生成邮箱和手机号OTP
        String emailOtp = otpGenerator.generate6DigitOtp();
        String phoneOtp = otpGenerator.generate6DigitOtp();

        // 存储OTP到Redis
        otpCacheService.saveEmailOtp(request.getEmail(), emailOtp);
        otpCacheService.savePhoneOtp(request.getPhone(), phoneOtp);

        // 异步发送OTP
        messageService.sendEmailOtp(request.getEmail(), emailOtp);
        messageService.sendSmsOtp(request.getPhone(), phoneOtp);

        return ResponseEntity.ok("验证码已发送,请查收");
    }

    // 第二步:验证OTP并创建用户
    @PostMapping("/register/verify")
    public ResponseEntity<String> verifyRegister(@RequestBody RegisterVerifyRequest request) {
        // 校验两个OTP
        boolean emailValid = otpCacheService.validateEmailOtp(request.getEmail(), request.getEmailOtp());
        boolean phoneValid = otpCacheService.validatePhoneOtp(request.getPhone(), request.getPhoneOtp());

        if (!emailValid || !phoneValid) {
            return ResponseEntity.badRequest().body("验证码无效或已过期");
        }

        // 创建用户(需加密密码)
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setPhone(request.getPhone());
        // 密码加密:user.setPassword(passwordEncoder.encode(request.getPassword()));
        userRepository.save(user);

        return ResponseEntity.ok("注册成功");
    }

    // 可选:重发OTP接口
    @PostMapping("/register/resend-otp")
    public ResponseEntity<String> resendOtp(@RequestBody ResendOtpRequest request) {
        // 此处可添加发送频率限制逻辑
        String emailOtp = otpGenerator.generate6DigitOtp();
        String phoneOtp = otpGenerator.generate6DigitOtp();
        otpCacheService.saveEmailOtp(request.getEmail(), emailOtp);
        otpCacheService.savePhoneOtp(request.getPhone(), phoneOtp);
        messageService.sendEmailOtp(request.getEmail(), emailOtp);
        messageService.sendSmsOtp(request.getPhone(), phoneOtp);
        return ResponseEntity.ok("验证码已重新发送");
    }

    // 定义请求DTO
    public static class RegisterInitRequest {
        private String email;
        private String phone;
        private String username;
        private String password;
        // getter/setter
    }

    public static class RegisterVerifyRequest {
        private String email;
        private String emailOtp;
        private String phone;
        private String phoneOtp;
        private String username;
        private String password;
        // getter/setter
    }

    public static class ResendOtpRequest {
        private String email;
        private String phone;
        // getter/setter
    }
}

前端(Angular)具体实现

1. 服务封装

创建AuthService处理后端接口调用:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private baseUrl = '/api/auth';

  constructor(private http: HttpClient) {}

  // 初始化注册,发送OTP
  initRegister(userData: {email: string, phone: string, username: string, password: string}): Observable<any> {
    return this.http.post(`${this.baseUrl}/register/init`, userData);
  }

  // 验证OTP并完成注册
  verifyRegister(verifyData: {email: string, emailOtp: string, phone: string, phoneOtp: string, username: string, password: string}): Observable<any> {
    return this.http.post(`${this.baseUrl}/register/verify`, verifyData);
  }

  // 重发OTP
  resendOtp(data: {email: string, phone: string}): Observable<any> {
    return this.http.post(`${this.baseUrl}/register/resend-otp`, data);
  }
}

2. 注册页面组件

实现两步式注册表单,包含倒计时和错误提示:

import { Component, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html'
})
export class RegisterComponent implements OnDestroy {
  currentStep = 1;
  registerForm: FormGroup;
  otpForm: FormGroup;
  countdown = 60;
  countdownTimer: any;

  constructor(private fb: FormBuilder, private authService: AuthService) {
    // 第一步表单:注册基本信息
    this.registerForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      phone: ['', [Validators.required, Validators.pattern('^1[3-9]\\d{9}$')]],
      username: ['', Validators.required],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });

    // 第二步表单:OTP输入
    this.otpForm = this.fb.group({
      emailOtp: ['', [Validators.required, Validators.pattern('^\\d{6}$')]],
      phoneOtp: ['', [Validators.required, Validators.pattern('^\\d{6}$')]]
    });
  }

  // 提交第一步,发送OTP
  submitRegisterInit() {
    if (this.registerForm.invalid) return;
    this.authService.initRegister(this.registerForm.value).subscribe({
      next: () => {
        this.currentStep = 2;
        this.startCountdown();
      },
      error: (err) => {
        alert(err.error || '发送验证码失败,请稍后重试');
      }
    });
  }

  // 提交第二步,验证OTP
  submitRegisterVerify() {
    if (this.otpForm.invalid) return;
    const verifyData = {
      ...this.registerForm.value,
      ...this.otpForm.value
    };
    this.authService.verifyRegister(verifyData).subscribe({
      next: () => {
        alert('注册成功!');
        // 跳转到登录页
        // this.router.navigate(['/login']);
      },
      error: (err) => {
        alert(err.error || '验证码无效或已过期');
      }
    });
  }

  // 重发OTP
  resendOtp() {
    const { email, phone } = this.registerForm.value;
    this.authService.resendOtp({ email, phone }).subscribe({
      next: () => {
        this.countdown = 60;
        this.startCountdown();
      },
      error: (err) => {
        alert(err.error || '重发验证码失败');
      }
    });
  }

  // 启动倒计时
  private startCountdown() {
    if (this.countdownTimer) clearInterval(this.countdownTimer);
    this.countdownTimer = setInterval(() => {
      this.countdown--;
      if (this.countdown <= 0) {
        clearInterval(this.countdownTimer);
      }
    }, 1000);
  }

  ngOnDestroy() {
    if (this.countdownTimer) clearInterval(this.countdownTimer);
  }
}

3. 页面模板示例

<div class="register-container">
  <!-- 第一步:注册信息输入 -->
  <div *ngIf="currentStep === 1">
    <h2>注册账号</h2>
    <form [formGroup]="registerForm" (ngSubmit)="submitRegisterInit()">
      <div>
        <label>邮箱:</label>
        <input type="email" formControlName="email">
        <span *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched">请输入有效邮箱</span>
      </div>
      <div>
        <label>手机号:</label>
        <input type="text" formControlName="phone">
        <span *ngIf="registerForm.get('phone')?.invalid && registerForm.get('phone')?.touched">请输入有效手机号</span>
      </div>
      <div>
        <label>用户名:</label>
        <input type="text" formControlName="username">
        <span *ngIf="registerForm.get('username')?.invalid && registerForm.get('username')?.touched">用户名不能为空</span>
      </div>
      <div>
        <label>密码:</label>
        <input type="password" formControlName="password">
        <span *ngIf="registerForm.get('password')?.invalid && registerForm.get('password')?.touched">密码至少6位</span>
      </div>
      <button type="submit" [disabled]="registerForm.invalid">发送验证码</button>
    </form>
  </div>

  <!-- 第二步:OTP验证 -->
  <div *ngIf="currentStep === 2">
    <h2>验证验证码</h2>
    <form [formGroup]="otpForm" (ngSubmit)="submitRegisterVerify()">
      <div>
        <label>邮箱验证码:</label>
        <input type="text" formControlName="emailOtp">
        <span *ngIf="otpForm.get('emailOtp')?.invalid && otpForm.get('emailOtp')?.touched">请输入6位数字验证码</span>
      </div>
      <div>
        <label>手机号验证码:</label>
        <input type="text" formControlName="phoneOtp">
        <span *ngIf="otpForm.get('phoneOtp')?.invalid && otpForm.get('phoneOtp')?.touched">请输入6位数字验证码</span>
      </div>
      <button type="submit" [disabled]="otpForm.invalid">完成注册</button>
      <button type="button" (click)="resendOtp()" [disabled]="countdown > 0">
        {{ countdown > 0 ? `${countdown}秒后重发` : '重发验证码' }}
      </button>
    </form>
  </div>
</div>

优化建议

  1. 密码加密:后端存储用户密码时,使用BCrypt等加密算法加密,避免明文存储
  2. 发送频率限制:在resendOtp接口中,用Redis记录同一邮箱/手机号的发送次数,如1分钟内最多允许3次请求
  3. 重试机制:对邮件/短信发送失败的情况,使用Spring Retry进行重试,提高送达率
  4. 日志监控:记录OTP发送、校验的日志,方便排查发送失败或校验异常问题
  5. 前端安全:添加验证码图形验证,防止机器人恶意请求OTP

内容的提问来源于stack exchange,提问作者Harshal Chopade

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 23:15:52