Skip to content

邮箱重置密码 ​

本文档详细介绍了系统中重置密码功能的接口设计、流程说明及代码实现。该功能主要分为两个步骤:

  1. 获取验证码:用户输入邮箱后,系统向该邮箱发送验证码;
  2. 重置密码:用户将邮箱、验证码及新密码提交到后端,后端验证验证码有效后更新密码。

1. 重置密码流程 ​

重置密码的流程主要分为以下两个步骤:

  1. 输入邮箱获取验证码
    用户通过提交邮箱地址,调用接口获取验证码,验证码将发送至指定邮箱。

  2. 提交邮箱、验证码、密码到后端
    用户将接收到的验证码、邮箱及新密码一起提交到后端接口,后端对验证码进行验证,验证成功后更新数据库中的用户密码。


2. 相关接口参数 ​

在重置密码功能中,主要涉及两个接口:

2.1 发送验证码接口 ​

调用该接口用于向指定邮箱发送验证码。

http
POST /api/v1/sendVerificationCode

{
    "email":"litongjava001@gmail.com"
}

2.2 重置密码接口 ​

调用该接口提交邮箱、验证码和新密码,完成密码重置。

http
POST /api/v1/user/resetPassword

{
    "email":"litongjava001@gmail.com",
    "password":"Litong@2516",
    "code":"108503"
}

文档中还提供了收到的邮件示例,邮件内容如下:

txt
Dear User,

Your code is:282531

If you did not request this verification, please disregard this email.

Best regards,
The College Bot AI Team

3. 代码实现 ​

以下部分代码展示了整个重置密码功能的实现细节,包括发送验证码、验证验证码以及更新密码等步骤。请注意,以下代码均未做修改,仅增加了解释说明。

3.1 邮箱验证码发送接口 ​

在代码中定义了一个 EmailRequest 类,用于接收邮箱地址的请求数据。

java
package nexus.io.tio.boot.admin.vo;

public class EmailRequest {
  private String email;

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }
}

独立提供的接口主要用于向指定邮箱发送验证码,客户端可以主动调用该接口(例如“重新发送验证码”)。

java
package nexus.io.tio.boot.admin.handler;

import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.admin.services.AppEmailService;
import nexus.io.tio.boot.admin.vo.EmailRequest;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.CORSUtils;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.json.JsonUtils;

public class EmailVerificationHandler {
  // 发送验证码邮件
  public HttpResponse sendVerificationCode(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    String origin = request.getOrigin();
    CORSUtils.enableCORS(response);
    String body = request.getBodyString();
    EmailRequest req = JsonUtils.parse(body, EmailRequest.class);

    AppEmailService emailService = Aop.get(AppEmailService.class);
    boolean sent = emailService.sendVerificationCodeEmail(req.getEmail(), origin);
    if (sent) {
      return response.setJson(RespBodyVo.ok());
    }
    return Resps.json(response, RespBodyVo.fail());
  }

  // 发送验证码邮件
  public HttpResponse sendVerification(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    String origin = request.getOrigin();
    CORSUtils.enableCORS(response);
    String body = request.getBodyString();
    EmailRequest req = JsonUtils.parse(body, EmailRequest.class);

    AppEmailService emailService = Aop.get(AppEmailService.class);
    boolean sent = emailService.sendVerificationEmail(req.getEmail(), origin);
    if (sent) {
      return response.setJson(RespBodyVo.ok());
    }
    return Resps.json(response, RespBodyVo.fail());
  }

  // 验证邮箱验证码
  public HttpResponse verifyEmail(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    CORSUtils.enableCORS(response);
    String email = request.getParameter("email");
    String code = request.getParameter("code");

    AppEmailService emailService = Aop.get(AppEmailService.class);
    boolean verified = emailService.verifyEmailCode(email, code);
    if (verified) {
      return Resps.json(response, RespBodyVo.ok());
    }
    return Resps.json(response, RespBodyVo.fail());
  }
}

3.1.2 邮件服务(AppEmailService) ​

AppEmailService 类负责生成验证码、保存验证码记录、发送邮件以及验证验证码是否正确且未过期。下面首先介绍验证码实体对象 EmailVerification:

java
package nexus.io.tio.boot.admin.vo;

import java.sql.Timestamp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmailVerification {
  private int id;
  private String email;
  private String verificationCode;
  private Timestamp createTime;
  private Timestamp expireTime;
  private boolean verified;
}

接下来是 AppEmailService 类的主要实现代码:

java
package nexus.io.tio.boot.admin.services;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import nexus.io.db.activerecord.Db;
import nexus.io.tio.boot.admin.vo.EmailVerification;
import nexus.io.tio.boot.email.EmailSender;
import nexus.io.tio.boot.server.TioBootServer;

public class AppEmailService {

  // 发送验证邮件,并在数据库保存验证码记录
  public boolean sendVerificationEmail(String email, String origin) {

    // 生成验证码(例如 6 位数字或随机字符串)
    String code = String.valueOf((int) (Math.random() * 900000 + 100000));
    // 设置过期时间(例如 15 分钟后)
    LocalDateTime expireTime = LocalDateTime.now().plusMinutes(15);
    OffsetDateTime atOffset = expireTime.atOffset(ZoneOffset.UTC);
    // 保存验证码记录
    String insertSql = "INSERT INTO app_email_verification (email, verification_code, expire_time) VALUES (?,?,?)";
    int rows = Db.updateBySql(insertSql, email, code, atOffset);
    if (rows > 0) {
      EmailSender emailSender = TioBootServer.me().getEmailSender();
      if (emailSender != null) {
        //
        return emailSender.sendVerificationEmail(email, origin, code);
      }
    }
    return false;
  }

  public boolean sendVerificationCodeEmail(String email, String origin) {
    // 生成验证码(例如 6 位数字或随机字符串)
    String code = String.valueOf((int) (Math.random() * 900000 + 100000));
    // 设置过期时间(例如 15 分钟后)
    LocalDateTime expireTime = LocalDateTime.now().plusMinutes(15);
    OffsetDateTime atOffset = expireTime.atOffset(ZoneOffset.UTC);
    // 保存验证码记录
    String insertSql = "INSERT INTO app_email_verification (email, verification_code, expire_time) VALUES (?,?,?)";
    int rows = Db.updateBySql(insertSql, email, code, atOffset);
    if (rows > 0) {
      EmailSender emailSender = TioBootServer.me().getEmailSender();
      if (emailSender != null) {
        //
        return emailSender.sendVerificationCodeEmail(email, origin, code);
      }
    }
    return false;
  }

  //验证邮箱验证码是否正确且未过期
  public boolean verifyEmailCode(String email, String code) {
    String sql = "SELECT * FROM app_email_verification WHERE email=? AND verification_code=? AND verified=FALSE AND expire_time > CURRENT_TIMESTAMP";
    EmailVerification ev = Db.findFirst(EmailVerification.class, sql, email, code);
    if (ev != null) {
      // 更新记录状态为已验证
      String updateSql = "UPDATE app_email_verification SET verified=TRUE WHERE id=?";
      Db.updateBySql(updateSql, ev.getId());
      // 同时更新用户表中 email_verified 字段(假设用户 id 与 email 相同)
      String updateUser = "UPDATE app_users SET email_verified=TRUE WHERE email=?";
      Db.updateBySql(updateUser, email);
      return true;
    }
    return false;
  }

}

在上面的代码中,验证码生成采用随机生成 6 位数字,同时设置验证码有效期为 15 分钟;在验证码验证成功后,不仅更新验证码的状态,还更新用户表中的 email_verified 字段,确保邮箱验证的一致性。


3.1.3 邮件发送实现(MyGetEmailSender) ​

MyGetEmailSender 类实现了 EmailSender 接口,负责调用实际的邮件发送工具(例如 LarkSuit 或 Mailjet)发送邮件。这里同时提供了发送验证邮件和验证码邮件的两种实现方式。

java
package nexus.io.myget.email;

import com.jfinal.kit.Kv;
import com.jfinal.template.Template;
import nexus.io.myget.utils.MailJetUtils;
import nexus.io.template.EmailEngine;
import nexus.io.tio.boot.admin.mail.LarkSuitEmailUtils;
import nexus.io.tio.boot.email.EmailSender;
import com.mailjet.client.MailjetResponse;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyGetEmailSender implements EmailSender {

  @Override
  public boolean send(String to, String subject, String content) {
    return false;
  }

  @Override
  public boolean sendVerificationEmail(String email, String origin, String code) {
    Template template = EmailEngine.getTemplate("register_mail.txt");
    String link = origin + "/verification/email?email=%s&code=%s";
    link = String.format(link, email, code);
    String content = template.renderToString(Kv.by("link", link));

    // 从邮箱地址中提取用户名作为收件人姓名
    String name = email.split("@")[0];

    String subject = "College Bot AI Email Verification";
    // 发送 HTML 格式的邀请邮件
    return sendWithLark(name, email, subject, content);
    //return sendWithMailjet(name, email, subject,content);
  }

  private boolean sendWithLark(String name, String email, String subject, String content) {
    try {
      LarkSuitEmailUtils.send(email, subject, content);
      return true;
    } catch (Exception e) {
      log.error(e.getMessage(), e);
      return false;
    }

  }

  private boolean sendWithMailjet(String name, String email, String subject, String content) {
    try {
      MailjetResponse response = MailJetUtils.sendText(name, email, subject, content);
      int status = response.getStatus();
      if (status == 200) {
        return true;
      } else {
        log.error(response.getRawResponseContent());
        return false;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public boolean sendVerificationCodeEmail(String email, String origin, String code) {
    Template template = EmailEngine.getTemplate("verification_code_mail.txt");
    String content = template.renderToString(Kv.by("code", code));

    // 从邮箱地址中提取用户名作为收件人姓名
    String name = email.split("@")[0];

    String subject = "College Bot AI Email Verification";
    // 发送 HTML 格式的邀请邮件
    return sendWithLark(name, email, subject, content);
  }
}

3.1.4 邮件模板(verification_code_mail.txt) ​

邮件模板文件内容如下,该模板用于发送验证码邮件。模板中使用占位符 #(code) 来插入实际验证码。

txt
Dear User,

Your code is:#(code)

If you did not request this verification, please disregard this email.

Best regards,
The College Bot AI Team

3.1.5 接口注册 ​

最后,通过路由将发送验证码接口注册到 /api/v1/sendVerificationCode 路径上。

java
r.add("/api/v1/sendVerificationCode", emailVerificationHandler::sendVerificationCode);

3.2 重置密码 ​

重置密码部分涉及到接收用户的请求数据、验证验证码、验证邮箱和密码格式,并更新用户密码。

3.2.1 请求对象 UserResetPasswordRequest ​

该对象用于封装重置密码请求的数据,包括邮箱、密码和验证码。

java
package nexus.io.tio.boot.admin.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResetPasswordRequest {
  private String email,password;
  private String code;
}

3.2.2 重置密码 Handler ​

在 resetPassword 方法中,将请求数据解析为 UserResetPasswordRequest 对象,并调用 AppUserService 进行密码重置处理。

java
public HttpResponse resetPassword(HttpRequest request) {
  HttpResponse response = TioRequestContext.getResponse();
  String bodyString = request.getBodyString();
  UserResetPasswordRequest userResetPassword = JsonUtils.parse(bodyString, UserResetPasswordRequest.class);

  AppUserService appUserService = Aop.get(AppUserService.class);
  RespBodyVo vo = appUserService.resetPassword(userResetPassword);
  return response.setJson(vo);
}

3.2.3 AppUserService 中的重置密码实现 ​

在 AppUserService 类中,resetPassword 方法主要完成以下步骤:

  • 数据验证:验证邮箱格式和密码格式是否正确;若不符合要求,返回失败信息。
  • 用户存在性判断:检查指定邮箱的用户是否存在(示例代码中有关于邮箱重复的判断)。
  • 验证码验证:调用 AppEmailService.verifyEmailCode 方法验证验证码是否正确且未过期。
  • 密码更新:生成密码盐和密码哈希值(采用 SHA-256 算法),更新数据库中对应的用户密码信息。
java
package nexus.io.tio.boot.admin.services;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.codec.digest.DigestUtils;

import nexus.io.db.activerecord.Db;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.model.validate.ValidateResult;
import nexus.io.tio.boot.admin.costants.AppConstant;
import nexus.io.tio.boot.admin.costants.TioBootAdminTableNames;
import nexus.io.tio.boot.admin.vo.AppUser;
import nexus.io.tio.boot.admin.vo.UserResetPasswordRequest;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.jwt.JwtUtils;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;
import nexus.io.tio.utils.validator.EmailValidator;
import nexus.io.tio.utils.validator.PasswordValidator;

public RespBodyVo resetPassword(UserResetPasswordRequest req) {

  List<ValidateResult> validateResults = new ArrayList<>();
  boolean ok = true;
  String email = req.getEmail();
  String code = req.getCode();
  boolean validate = EmailValidator.validate(email);
  if (!validate) {
    ValidateResult validateResult = ValidateResult.by("eamil", "Failed to valiate email:" + email);
    validateResults.add(validateResult);
    ok = false;
  }

  String password = req.getPassword();
  validate = PasswordValidator.validate(password);
  if (!validate) {
    ValidateResult validateResult = ValidateResult.by("password", "Failed to valiate password:" + password);
    validateResults.add(validateResult);
    ok = false;
  }

  if (!ok) {
    return RespBodyVo.failData(validateResults);
  }

  boolean exists = Db.exists(TioBootAdminTableNames.app_users, "email", email);
  if (exists) {
    ValidateResult validateResult = ValidateResult.by("eamil", "eamil already taken" + email);
    validateResults.add(validateResult);
  }

  if (!ok) {
    return RespBodyVo.failData(validateResults);
  }

  boolean verify = Aop.get(AppEmailService.class).verifyEmailCode(email, code);
  if (!verify) {
    return RespBodyVo.fail("Failed to verify code");
  }

  // 生成加盐字符串(示例中直接使用随机数,实际可使用更复杂逻辑)
  String salt = String.valueOf(System.currentTimeMillis());
  // 生成密码哈希(密码+盐)
  String passwordHash = DigestUtils.sha256Hex(password + salt);

  String updateSql = "update app_users set password_salt=?, password_hash=? where email=?";
  Db.updateBySql(updateSql, salt, passwordHash, email);
  return RespBodyVo.ok();
}

3.2.4 接口注册 ​

将重置密码接口注册到 /api/v1/user/resetPassword 路径上,确保前端调用时能够正确访问到该功能。

java
r.add("/api/v1/user/resetPassword", appUserHandler::resetPassword);

总结 ​

本文档对重置密码功能的实现进行了详细介绍,包含以下几个方面:

  • 流程说明:从获取验证码到重置密码的整体流程说明;
  • 接口说明:详细描述了发送验证码和重置密码的请求参数;
  • 代码实现:分别展示了邮箱验证码发送、邮件服务实现、验证码验证以及用户密码更新的具体代码;
  • 技术要点:包括验证码生成、有效期控制、密码盐及哈希计算、数据验证等关键技术实现点。

通过本文档,开发者可以全面了解并掌握重置密码功能的实现逻辑,为后续系统维护和功能扩展提供了有力的支持。

MIT Licensed