首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Spring Boot 實現郵箱註冊激活驗證完整流程:JWT Token + Redis + 異步發信

     編輯:Java 程式設計入門

業務流程設計

  1. 用戶提交註冊:前端發送郵箱、密碼等信息
  2. 後端驗證並持久化:校驗郵箱格式、唯一性,保存用戶狀態為 INACTIVE
  3. 生成激活 Token:使用 JWT 包含 userId、email、過期時間 (如 24h),簽名防篡改
  4. 發送激活郵件:異步發送包含激活鏈接的 HTML 郵件
  5. 用戶點擊鏈接:前端頁面調用激活接口攜帶 Token
  6. 驗證 Token 並激活:解析 JWT、校驗簽名/過期/用戶狀態,更新為 ACTIVE
  7. Token 失效:激活成功後標記 Token 已用 (Redis 黑名單或刪除)

技術棧選型

  • 框架:Spring Boot 3.x
  • 安全:JWT (jjwt 0.12+)
  • 緩存:Redis (存儲 Token 黑名單、限流計數)
  • 郵件:Spring Mail + Thymeleaf 模板 + @Async 異步
  • 數據庫:MySQL + MyBatis-Plus / JPA

核心代碼實現

1. 實體類


@Entity
@Table(name = "sys_user")
@Data
@Builder
@NoArgsConstructor @AllArgsConstructor
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false, length = 100)
    private String email;
    
    @Column(nullable = false, length = 100)
    private String password;  // BCrypt 加密存儲
    
    @Column(nullable = false, length = 50)
    private String username;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private UserStatus status = UserStatus.INACTIVE;  // INACTIVE, ACTIVE, LOCKED
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "activated_at")
    private LocalDateTime activatedAt;
    
    public enum UserStatus { INACTIVE, ACTIVE, LOCKED }
}

2. JWT Token 工具類


@Component
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtTokenProvider {
    private String secret;      // Base64 編碼的密鑰,至少 256 位
    private long expirationMs;  // 過期時間,如 86400000 (24h)
    private String issuer;      // 簽發者
    
    private Key key;
    
    @PostConstruct
    public void init() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
    
    // 生成激活 Token
    public String generateActivationToken(User user) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + expirationMs);
        
        return Jwts.builder()
            .setSubject(user.getEmail())
            .claim("userId", user.getId())
            .claim("type", "ACTIVATION")
            .setIssuer(issuer)
            .setIssuedAt(now)
            .setExpiration(expiry)
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }
    
    // 解析並驗證 Token
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(key)
            .requireIssuer(issuer)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
    
    // 校驗 Token 有效性
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    // 從 Token 獲取用戶 ID
    public Long getUserIdFromToken(String token) {
        return parseToken(token).get("userId", Long.class);
    }
}

3. Redis Token 黑名單服務


@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
    private final RedisTemplate redisTemplate;
    private static final String BLACKLIST_PREFIX = "token:blacklist:";
    
    // 加入黑名單 (激活成功後調用)
    public void blacklist(String token, long ttlMs) {
        String key = BLACKLIST_PREFIX + token;
        redisTemplate.opsForValue().set(key, "1", ttlMs, TimeUnit.MILLISECONDS);
    }
    
    // 檢查是否在黑名單
    public boolean isBlacklisted(String token) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token));
    }
}

4. 異步郵件發送服務


@Service
@RequiredArgsConstructor
public class EmailService {
    private final JavaMailSender mailSender;
    private final SpringTemplateEngine templateEngine;
    private final JwtTokenProvider jwtProvider;
    
    @Value("${app.base-url}")
    private String baseUrl;  // 如 https://api.example.com
    
    @Async("emailExecutor")  // 使用專用線程池
    public void sendActivationEmail(User user) {
        String token = jwtProvider.generateActivationToken(user);
        String activationUrl = baseUrl + "/api/auth/activate?token=" + token;
        
        Context context = new Context();
        context.setVariable("username", user.getUsername());
        context.setVariable("activationUrl", activationUrl);
        context.setVariable("expireHours", jwtProvider.getExpirationMs() / 3600000);
        
        String htmlContent = templateEngine.process("email/activation", context);
        
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
            helper.setTo(user.getEmail());
            helper.setSubject("歡迎註冊 - 請激活您的賬號");
            helper.setText(htmlContent, true);  // true = HTML
            mailSender.send(message);
            log.info("激活郵件已發送至: {}", user.getEmail());
        } catch (MessagingException e) {
            log.error("郵件發送失敗: {}", user.getEmail(), e);
            // 可加入重試隊列或告警
        }
    }
}

5. 郵件模板 (Thymeleaf)






    
    


    

歡迎加入 用戶!

感謝註冊,請點擊下方按鈕激活賬號 (鏈接 24 小時有效):

立即激活賬號

若按鈕無法點擊,請複製鏈接至瀏覽器:

鏈接

6. 註冊與激活控制器


@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Validated
public class AuthController {
    private final UserService userService;
    private final EmailService emailService;
    private final JwtTokenProvider jwtProvider;
    private final TokenBlacklistService blacklistService;
    
    @PostMapping("/register")
    public Result register(@Valid @RequestBody RegisterDTO dto) {
        // 1. 校驗郵箱唯一性
        if (userService.existsByEmail(dto.getEmail())) {
            return Result.error("郵箱已被註冊");
        }
        
        // 2. 創建用戶 (密碼加密)
        User user = User.builder()
            .email(dto.getEmail())
            .username(dto.getUsername())
            .password(passwordEncoder.encode(dto.getPassword()))
            .status(User.UserStatus.INACTIVE)
            .createdAt(LocalDateTime.now())
            .build();
        userService.save(user);
        
        // 3. 異步發送激活郵件
        emailService.sendActivationEmail(user);
        
        return Result.success("註冊成功,請查收郵件激活賬號");
    }
    
    @GetMapping("/activate")
    public Result activate(@RequestParam @NotBlank String token) {
        // 1. 基礎驗證
        if (!jwtProvider.validateToken(token)) {
            return Result.error("激活鏈接無效或已過期");
        }
        if (blacklistService.isBlacklisted(token)) {
            return Result.error("激活鏈接已使用");
        }
        
        // 2. 解析用戶 ID
        Long userId = jwtProvider.getUserIdFromToken(token);
        User user = userService.getById(userId);
        if (user == null) {
            return Result.error("用戶不存在");
        }
        if (user.getStatus() == User.UserStatus.ACTIVE) {
            return Result.error("賬號已激活,請直接登錄");
        }
        
        // 3. 激活用戶
        user.setStatus(User.UserStatus.ACTIVE);
        user.setActivatedAt(LocalDateTime.now());
        userService.updateById(user);
        
        // 4. Token 加入黑名單 (防止重複使用)
        blacklistService.blacklist(token, jwtProvider.getExpirationMs());
        
        return Result.success("激活成功,請登錄");
    }
}

7. 線程池配置


@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean("emailExecutor")
    public Executor emailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("email-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

8. 配置文件 (application.yml)


app:
  jwt:
    secret: "your-base64-encoded-256-bit-secret-key-here"  # 生產環境用環境變量注入
    expiration-ms: 86400000  # 24小時
    issuer: "my-app"
  base-url: "https://api.yourdomain.com"

spring:
  mail:
    host: smtp.exmail.qq.com
    port: 465
    username: noreply@yourdomain.com
    password: ${MAIL_PASSWORD}  # 環境變量
    protocol: smtps
    properties:
      mail:
        smtp:
          ssl:
            enable: true
          auth: true
          timeout: 10000
  redis:
    host: localhost
    port: 6379
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    mode: HTML

安全性加固清單

  1. Token 簽名算法:HS256 (對稱) 或 RS256 (非對稱,私鑰簽名/公鑰驗證)
  2. 密鑰管理:Base64 編碼存儲於環境變量/配置中心,定期輪換
  3. 防暴力註冊:Redis 限流 register:ip:{ip} 計數,超過閾值封禁 1h
  4. 防郵件枚舉:註冊接口統一返回「註冊成功請查收郵件」,不暴露郵箱是否存在
  5. HTTPS 強制:激活鏈接必須 HTTPS,防止 Token 劫持
  6. Token 單次使用:Redis 黑名單 + 短 TTL,激活即失效
  7. 輸入驗證:@Email、@Size、@Pattern 校驗 DTO

常見問題排查

❌ 郵件發送超時/失敗

  • 檢查 SMTP 主機/端口/協議 (465 SSL / 587 STARTTLS)
  • 企業郵箱需開啟 SMTP 服務並獲取授權碼 (非登錄密碼)
  • 雲服務器 25 端口常被封禁,改用 465/587

❌ Token 解析報 MalformedJwtException

  • 鏈接中 Token 被 URL 編碼導致截斷 → 前端 encodeURIComponent(token)
  • 密鑰長度不足 (HS256 需 ≥ 256 位)

❌ 激活頁面白屏/報錯

  • 前端路由未匹配 /activate 路徑
  • Token 參數名不一致 (query vs params)

擴展:手機號驗證碼註冊

同理,將 JWT 中 type 改為 PHONE_VERIFY,發送短信而非郵件,激活接口驗證短信驗證碼 (存儲於 Redis,5 分鐘過期)。

總結

本方案採用 JWT 無狀態 Token + Redis 黑名單 + 異步郵件 架構,具備高性能、水平擴展、安全可靠特點。核心優勢:無需數據庫存儲激活碼、Token 自帶過期時間、異步不阻塞註冊響應、模板引擎靈活定制郵件內容。生產部署時注意密鑰管理、HTTPS 強制、限流防刷,即可構建企業級用戶註冊激活體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved