获取邮箱验证码功能

pull/5/head
杨默涵 2 months ago
parent 9d7116793b
commit 8a8a03839c

@ -38,6 +38,16 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
<build>

@ -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);
}
}
}

@ -20,6 +20,11 @@ public class ExamController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
@FXML private Button loginButton;
@FXML private TextField emailField;
@FXML private TextField emailCodeField;
@FXML private Button sendCodeButton;
@FXML private Button verifyCodeButton;
@FXML private Label emailStatusLabel;
@FXML private Label loginStatusLabel;
// 考试设置界面控件
@ -39,6 +44,8 @@ public class ExamController {
private Map<Integer, String> userAnswers = new HashMap<>();
private ChoiceQuestionGenerator questionGenerator;
private final Map<String, Account> userMap = new HashMap<>();
private final EmailCodeService emailCodeService = new EmailCodeService();
private boolean emailVerified = false;
// 常量定义
private static final int MIN_QUESTIONS = 5;
@ -50,6 +57,9 @@ public class ExamController {
setupLevelComboBox();
examSetupPanel.setVisible(false);
questionCountField.setText("10");
if (emailStatusLabel != null) {
emailStatusLabel.setText("");
}
}
private void initAccounts() {
@ -93,6 +103,11 @@ public class ExamController {
examSetupPanel.setVisible(true);
loginStatusLabel.setText("登录成功!");
loginStatusLabel.setStyle("-fx-text-fill: green;");
emailVerified = false;
if (emailStatusLabel != null) {
emailStatusLabel.setText("请进行邮箱验证后再开始考试");
emailStatusLabel.setStyle("-fx-text-fill: #8B0000;");
}
} else {
loginStatusLabel.setText("用户名或密码错误");
loginStatusLabel.setStyle("-fx-text-fill: red;");
@ -110,6 +125,10 @@ public class ExamController {
passwordField.clear();
loginStatusLabel.setText("");
statusLabel.setText("");
emailVerified = false;
if (emailField != null) emailField.clear();
if (emailCodeField != null) emailCodeField.clear();
if (emailStatusLabel != null) emailStatusLabel.setText("");
}
@FXML
@ -132,6 +151,12 @@ public class ExamController {
String selectedLevel = levelComboBox.getValue();
ChoiceQuestionGenerator.Level level = ChoiceQuestionGenerator.Level.valueOf(selectedLevel);
if (!emailVerified) {
statusLabel.setText("请先完成邮箱验证码验证");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 生成考试题目
questionGenerator = new ChoiceQuestionGenerator();
examQuestions = questionGenerator.generateQuestions(level, count);
@ -150,6 +175,57 @@ public class ExamController {
statusLabel.setStyle("-fx-text-fill: red;");
}
}
@FXML
private void handleSendEmailCode() {
String email = emailField != null ? emailField.getText().trim() : "";
if (email.isEmpty()) {
if (emailStatusLabel != null) {
emailStatusLabel.setText("请输入邮箱");
emailStatusLabel.setStyle("-fx-text-fill: red;");
}
return;
}
try {
emailCodeService.sendCode(email);
if (emailStatusLabel != null) {
emailStatusLabel.setText("验证码已发送请在5分钟内查收控制台可见");
emailStatusLabel.setStyle("-fx-text-fill: green;");
}
} catch (Exception ex) {
if (emailStatusLabel != null) {
emailStatusLabel.setText("发送失败:" + ex.getMessage());
emailStatusLabel.setStyle("-fx-text-fill: red;");
}
}
}
@FXML
private void handleVerifyEmailCode() {
String email = emailField != null ? emailField.getText().trim() : "";
String code = emailCodeField != null ? emailCodeField.getText().trim() : "";
if (email.isEmpty() || code.isEmpty()) {
if (emailStatusLabel != null) {
emailStatusLabel.setText("请输入邮箱和验证码");
emailStatusLabel.setStyle("-fx-text-fill: red;");
}
return;
}
boolean ok = emailCodeService.verifyCode(email, code);
if (ok) {
emailVerified = true;
if (emailStatusLabel != null) {
emailStatusLabel.setText("邮箱验证成功,可开始考试");
emailStatusLabel.setStyle("-fx-text-fill: green;");
}
} else {
emailVerified = false;
if (emailStatusLabel != null) {
emailStatusLabel.setText("验证码错误或已过期");
emailStatusLabel.setStyle("-fx-text-fill: red;");
}
}
}
private void startExam() {
try {

@ -11,7 +11,7 @@ public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("exam-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 900, 800);
Scene scene = new Scene(fxmlLoader.load(), 900, 900);
stage.setTitle("数学考试系统");
stage.setResizable(true);
stage.setScene(scene);

@ -1,6 +1,8 @@
module com.example.mathsystemtogether {
requires javafx.controls;
requires javafx.fxml;
requires jakarta.mail;
requires jakarta.activation;
opens com.example.mathsystemtogether to javafx.fxml;

@ -55,6 +55,25 @@
</Label>
<PasswordField fx:id="passwordField" promptText="请输入密码" prefWidth="180.0" style="-fx-background-color: white; -fx-background-radius: 8; -fx-border-color: #8A2BE2; -fx-border-radius: 8; -fx-border-width: 2; -fx-padding: 8;"/>
</HBox>
<HBox spacing="15.0" alignment="CENTER_LEFT">
<Label text="📧 邮箱:" minWidth="80.0" textFill="#6A0DAD">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<TextField fx:id="emailField" promptText="请输入邮箱" prefWidth="180.0" style="-fx-background-color: white; -fx-background-radius: 8; -fx-border-color: #8A2BE2; -fx-border-radius: 8; -fx-border-width: 2; -fx-padding: 8;"/>
<Button fx:id="sendCodeButton" text="发送验证码" onAction="#handleSendEmailCode" style="-fx-background-color: linear-gradient(to right, #8A2BE2, #9932CC); -fx-text-fill: white; -fx-background-radius: 10; -fx-padding: 8 16; -fx-font-size: 12; -fx-font-weight: bold; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 5, 0, 0, 2);"/>
</HBox>
<HBox spacing="15.0" alignment="CENTER_LEFT">
<Label text="🔏 验证码:" minWidth="80.0" textFill="#6A0DAD">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<TextField fx:id="emailCodeField" promptText="请输入6位验证码" prefWidth="180.0" style="-fx-background-color: white; -fx-background-radius: 8; -fx-border-color: #8A2BE2; -fx-border-radius: 8; -fx-border-width: 2; -fx-padding: 8;"/>
<Button fx:id="verifyCodeButton" text="验证" onAction="#handleVerifyEmailCode" style="-fx-background-color: linear-gradient(to right, #8A2BE2, #9932CC); -fx-text-fill: white; -fx-background-radius: 10; -fx-padding: 8 16; -fx-font-size: 12; -fx-font-weight: bold; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 5, 0, 0, 2);"/>
<Label fx:id="emailStatusLabel" textFill="#8B0000" style="-fx-font-weight: bold;"/>
</HBox>
<HBox spacing="15.0" alignment="CENTER_LEFT">
<Button fx:id="loginButton" text="🚀 登录" onAction="#handleLogin" style="-fx-background-color: linear-gradient(to right, #8A2BE2, #9932CC); -fx-text-fill: white; -fx-background-radius: 10; -fx-padding: 10 20; -fx-font-size: 14; -fx-font-weight: bold; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 5, 0, 0, 2);"/>
<Label fx:id="loginStatusLabel" textFill="#8B0000" style="-fx-font-weight: bold;"/>

@ -0,0 +1,9 @@
mail.from=1961004835@qq.com
mail.authCode=nixyzbpbcrekhgfe
mail.host=smtp.qq.com
mail.port=465
mail.ssl=true
mail.subject=您的验证码
mail.bodyPrefix=验证码:
mail.bodySuffix=5分钟内有效。
Loading…
Cancel
Save