|
|
|
|
@ -0,0 +1,125 @@
|
|
|
|
|
package com.example.mathsystemtogether;
|
|
|
|
|
|
|
|
|
|
import java.time.Duration;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Properties;
|
|
|
|
|
import java.util.Random;
|
|
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
import jakarta.mail.Authenticator;
|
|
|
|
|
import jakarta.mail.Message;
|
|
|
|
|
import jakarta.mail.MessagingException;
|
|
|
|
|
import jakarta.mail.PasswordAuthentication;
|
|
|
|
|
import jakarta.mail.Session;
|
|
|
|
|
import jakarta.mail.Transport;
|
|
|
|
|
import jakarta.mail.internet.InternetAddress;
|
|
|
|
|
import jakarta.mail.internet.MimeMessage;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 简单的内存版邮箱验证码服务(单进程适用)。
|
|
|
|
|
* 如需真实发信,可将 sendEmail 方法接入 SMTP 或第三方服务。
|
|
|
|
|
*/
|
|
|
|
|
public class EmailCodeService {
|
|
|
|
|
|
|
|
|
|
private static class CodeRecord {
|
|
|
|
|
final String code;
|
|
|
|
|
final Instant expireAt;
|
|
|
|
|
|
|
|
|
|
CodeRecord(String code, Instant expireAt) {
|
|
|
|
|
this.code = code;
|
|
|
|
|
this.expireAt = expireAt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final Map<String, CodeRecord> emailToCode = new ConcurrentHashMap<>();
|
|
|
|
|
private final Map<String, Instant> emailToLastSend = new ConcurrentHashMap<>();
|
|
|
|
|
private final Random random = new Random();
|
|
|
|
|
|
|
|
|
|
private final Duration codeTtl = Duration.ofMinutes(5);
|
|
|
|
|
private final Duration rateLimit = Duration.ofSeconds(60);
|
|
|
|
|
|
|
|
|
|
public void sendCode(String email) {
|
|
|
|
|
Instant now = Instant.now();
|
|
|
|
|
Instant last = emailToLastSend.get(email);
|
|
|
|
|
if (last != null && Duration.between(last, now).compareTo(rateLimit) < 0) {
|
|
|
|
|
long waitSeconds = rateLimit.minus(Duration.between(last, now)).getSeconds();
|
|
|
|
|
throw new IllegalStateException("请求过于频繁,请于" + waitSeconds + "秒后重试");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String code = generateCode();
|
|
|
|
|
emailToCode.put(email, new CodeRecord(code, now.plus(codeTtl)));
|
|
|
|
|
emailToLastSend.put(email, now);
|
|
|
|
|
|
|
|
|
|
// 使用 SMTP 发送邮件(QQ邮箱)
|
|
|
|
|
sendEmail(email, code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean verifyCode(String email, String code) {
|
|
|
|
|
CodeRecord record = emailToCode.get(email);
|
|
|
|
|
if (record == null) return false;
|
|
|
|
|
if (Instant.now().isAfter(record.expireAt)) {
|
|
|
|
|
emailToCode.remove(email);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
boolean ok = record.code.equals(code);
|
|
|
|
|
if (ok) {
|
|
|
|
|
emailToCode.remove(email); // 一次性
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String generateCode() {
|
|
|
|
|
return String.valueOf(100000 + random.nextInt(900000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void sendEmail(String email, String code) {
|
|
|
|
|
// 从资源文件读取发件配置
|
|
|
|
|
Properties conf = new Properties();
|
|
|
|
|
try {
|
|
|
|
|
conf.load(EmailCodeService.class.getResourceAsStream("/com/example/mathsystemtogether/mail.properties"));
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
System.out.println("[EmailCodeService] 读取 mail.properties 失败,改为控制台输出验证码:" + code);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String from = conf.getProperty("mail.from", "");
|
|
|
|
|
String authCode = conf.getProperty("mail.authCode", "");
|
|
|
|
|
String host = conf.getProperty("mail.host", "smtp.qq.com");
|
|
|
|
|
String port = conf.getProperty("mail.port", "465");
|
|
|
|
|
boolean ssl = Boolean.parseBoolean(conf.getProperty("mail.ssl", "true"));
|
|
|
|
|
String subject = conf.getProperty("mail.subject", "您的验证码");
|
|
|
|
|
String bodyPrefix = conf.getProperty("mail.bodyPrefix", "验证码:");
|
|
|
|
|
String bodySuffix = conf.getProperty("mail.bodySuffix", ",5分钟内有效。");
|
|
|
|
|
|
|
|
|
|
if (from.isEmpty() || authCode.isEmpty()) {
|
|
|
|
|
System.out.println("[EmailCodeService] 未配置 mail.from 或 mail.authCode,改为控制台输出验证码:" + code);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Properties props = new Properties();
|
|
|
|
|
props.put("mail.smtp.host", host);
|
|
|
|
|
props.put("mail.smtp.port", port);
|
|
|
|
|
props.put("mail.smtp.auth", "true");
|
|
|
|
|
props.put("mail.smtp.ssl.enable", String.valueOf(ssl));
|
|
|
|
|
|
|
|
|
|
Session session = Session.getInstance(props, new Authenticator() {
|
|
|
|
|
@Override
|
|
|
|
|
protected PasswordAuthentication getPasswordAuthentication() {
|
|
|
|
|
return new PasswordAuthentication(from, authCode);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
Message message = new MimeMessage(session);
|
|
|
|
|
message.setFrom(new InternetAddress(from));
|
|
|
|
|
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email));
|
|
|
|
|
message.setSubject(subject);
|
|
|
|
|
message.setText(bodyPrefix + code + bodySuffix);
|
|
|
|
|
Transport.send(message);
|
|
|
|
|
} catch (MessagingException e) {
|
|
|
|
|
throw new IllegalStateException("邮件发送失败:" + e.getMessage(), e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|