前言
本文主要介绍缺陷跟踪系统项目中涉及的邮箱验证码找回密码功能的实现,效果图如下:
后端实现
1.数据库表设计
实现邮箱验证码功能只需设计一张用户表,用到邮箱、密码、加密盐,这几个字段。其他字段根据项目需要自己额外设计。
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
`realname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
`secret_key` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码密钥',
`email` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系方式',
`gender` tinyint(1) NULL DEFAULT NULL COMMENT '性别,0男1女',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '账号状态,0注销1正常2冻结',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `index_phone_unique`(`phone`) USING BTREE,
UNIQUE INDEX `index_username_unique`(`username`) USING BTREE,
UNIQUE INDEX `email_UNIQUE`(`email`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
2.安装并启动Redis
在该功能的实现中,Redis主要用来存储请求权限码和邮箱验证码。
下载并安装Redis后,输入启动命令
redis-server.exe redis.windows.conf
如果出现以下界面,则成功启动,成功启动后窗口不能关闭。
3.添加依赖
<!--spring-boot启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring-boot web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!--mybatis-plus启动依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 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>
4.编写配置文件
编写application配置文件,配置数据库相关信息。(此处不展示)
编写application-email.yml 配置文件,配置邮箱的相关信息。
spring:
mail:
host: smtp.qq.com
username: qq邮箱
password: 授权码
protocol: smtp
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
enable: true
此外,实现邮箱发送功能,需要开启SMTP服务,在QQ邮箱中,点击设置 -> 点击账户 -> 开启SMTP服务 -> 获取授权码,将授权码复制到上面的 application-email.yml 文件里。
5.User实体类
在entity 包下,创建实体类
@Getter
@Setter
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
//id
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
//用户名
private String username;
//密码
private String password;
//密码密钥
private String secretKey;
//邮箱
private String email;
}
6.vo类
创建Result统一接口返回类
@Data
@AllArgsConstructor
public class Result<T> {
//代码
private Integer code;
//消息
private String msg;
//数据
private Map<String,T> data = new HashMap<>();
private Result() {}
// 成功
public static Result success() {
Result result = new Result();
result.setCode(200);
result.setMsg("成功");
return result;
}
public static Result success(String message) {
Result result = new Result();
result.setCode(200);
result.setMsg(message);
return result;
}
public static Result success(int code, String message) {
Result result = new Result();
result.setCode(code);
result.setMsg(message);
return result;
}
// 失败
public static Result error() {
Result result = new Result();
result.setCode(400);
result.setMsg("失败");
return result;
}
// 失败
public static Result error(String message) {
Result result = new Result();
result.setCode(400);
result.setMsg(message);
return result;
}
public static Result error(int code, String message) {
Result result = new Result();
result.setCode(code);
result.setMsg(message);
return result;
}
public Result data(String key, T value){
this.data.put(key, value);
return this;
}
public Result data(Map<String, T> map){
this.setData(map);
return this;
}
}
参数类FindPasswordInfo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FindPasswordInfo {
private String email; // 邮箱
private String password; // 密码
private String passwordConfirm; // 确认密码
private String code; // 验证码
}
7.工具类
编写StringUtil工具类,进行随机验证码生成、邮箱校验。
public class StringUtil {
/**
* 邮箱校验
*
* @param email 邮箱
* @return true or false
*/
public static boolean checkEmail(String email) {
String check = "^([a-zA-Z]|[0-9])(\\w|\\-)+@[a-zA-Z0-9]+\\.([a-zA-Z]{2,4})$";
Pattern regex = Pattern.compile(check);
Matcher matcher = regex.matcher(email);
return matcher.matches();
}
/**
* 随机生成六位数字验证码
*/
public static String randomSixCode() {
return String.valueOf(new Random().nextInt(899999) + 100000);
}
/**
* 密码校验(长度 6-18,至少包含1个字母)
* @param password
* @return
*/
public static boolean checkPassword(String password) {
String check = "(?=.*[a-zA-Z])[a-zA-Z0-9]{6,18}";
Pattern regex = Pattern.compile(check);
Matcher matcher = regex.matcher(password);
return matcher.matches();
}
public static String getUserName(){
String number = String.valueOf(new Random().nextInt(89999999) + 10000000);
return "u"+number;
}
}
编写SaltUtil类,进行密码加密。
public class SaltUtil {
/**
* 随机生成盐值工具
* @param n
* @return
*/
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < n; i++) {
char c = chars[new Random().nextInt(chars.length)];
stringBuilder.append(c);
}
return stringBuilder.toString();
}
/**
* 随机生成盐值,对用户密码进行加密
* @param password
* @return
*/
//密码加密
public static String createCredentials(String password, String salt) {
SimpleHash simpleHash = new SimpleHash("MD5", password, salt, 1024);
return simpleHash.toHex();
}
}
8.邮箱服务类
@Service
public class MailServiceImpl implements MailService {
@Resource
private JavaMailSender javaMailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送简单的邮箱
*
* @param to 收件人
* @param theme 标题
* @param content 正文内容
* @param cc 抄送
*/
public void sendSimpleMail(String to, String theme, String content, String... cc) {
// 创建邮件对象
SimpleMailMessage message = new SimpleMailMessage();
try {
message.setFrom(String.valueOf(new InternetAddress(from, "缺陷跟踪系统", "UTF-8"))); // 发件人
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
message.setTo(to); // 收件人
message.setSubject(theme); // 标题
message.setText(content); // 内容
if (ArrayUtils.isNotEmpty(cc)) {
message.setCc(cc);
}
// 发送
javaMailSender.send(message);
}
/**
* 发送HTML邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
public void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
if (ArrayUtils.isNotEmpty(cc)) {
helper.setCc(cc);
}
javaMailSender.send(message);
}
}
9.配置线程服务
发送邮箱的操作比较耗时间,我们需要开辟一个线程去执行。如果在主线程执行的话,可能会等待较长时间导致请求超时。
创建 ThreadPoolConfig线程池配置类用于统一管理线程。
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核兴线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 设置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间 60s
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("缺陷跟踪系统");
// 是否所有任务执行完毕后关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 执行初始化
executor.initialize();
return executor;
}
}
创建 threadService 线程服务类
@Service
public class ThreadServiceImpl implements ThreadService {
@Autowired
private MailService mailService;
/**
* 发送邮箱
* @param to 收件人
* @param theme 主题
* @param content 内容
*/
@Async("taskExecutor")
public void sendSimpleMail(String to, String theme, String content) {
mailService.sendSimpleMail(to, theme, content);
}
}
10.验证码服务类
为了防止发送验证码的接口被故意重复调用,需要使用权限码进行校验,在每次发送验证码请求前都先请求一个随机的权限码。只有权限码有效时,才会发送邮箱验证码。
当前端点击获取验证码按钮时,会先发送请求权限码。后端收到请求并且邮箱校验通过之后,就会随机生成一个权限码并回传给前端,同时存入Redis中进行缓存,设置有效时间为10s(可根据实际情况设置)。前端收到权限码后将权限码和邮箱作为参数,调用发送验证码的请求,后端校验邮箱和权限码是否正确,如正确则开辟线程发送6位验证码到邮箱。
public interface VerificationCodeService {
/**
* 获取请求权限码
* 使用权限码校验防止故意重复使用该接口
* @param info (邮箱)
* @return
*/
Result getRequestPermissionCode(FindPasswordInfo info);
/**
* 发送邮箱验证码
* @param info (邮箱和权限码)
* @return
*/
Result sendEmailCode(FindPasswordInfo info);
}
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ThreadService threadService;
@Override
public Result getRequestPermissionCode(FindPasswordInfo info) {
// 非空校验
if (StringUtils.isBlank(info.getEmail()))
return Result.error("参数格式不正确");
//邮箱校验
if (!StringUtil.checkEmail(info.getEmail())) {
return Result.error("邮箱格式不正确");
}
// 随机生成权限码
String permissionCode = UUID.randomUUID().toString();
System.out.println(permissionCode);
// 存入redis,缓存10s
redisTemplate.opsForValue().set("EMAIL_REQUEST_VERIFY_" + info.getEmail(), permissionCode, 20, TimeUnit.SECONDS);
return Result.success().data("permissionCode", permissionCode);
}
@Override
public Result sendEmailCode(FindPasswordInfo info) {
if (info == null)
return Result.error("参数格式不正确");
//获取邮箱和权限码
String email = info.getEmail();
String permissionCode = info.getCode();
// 参数校验
if (StringUtils.isBlank(email) && StringUtils.isBlank(permissionCode)) {
return Result.error("参数格式不正确");
}else if (!StringUtil.checkEmail(email)) {
// 邮箱校验
return Result.error("邮箱格式不正确");
}else {
// 权限码比对
String rightCode = redisTemplate.opsForValue().get("EMAIL_REQUEST_VERIFY_" + email);
if (!permissionCode.equals(rightCode)) {
// 不通过
return Result.error("非法操作");
}
}
// 全部通过
// 随机生成6位数字验证码
String code = StringUtil.randomSixCode();
// 正文内容
String content = "亲爱的用户:\n" +
"您此次的验证码为:\n\n" +
code + "\n\n" +
"此验证码5分钟内有效,请立即进行下一步操作。 如非你本人操作,请忽略此邮件。\n" +
"感谢您的使用!";
// 发送验证码
threadService.sendSimpleMail(email, "您此次的验证码为:" + code, content);
// 丢入缓存,设置5分钟过期
redisTemplate.opsForValue().set("EMAIL_" + email, code, 5*60, TimeUnit.SECONDS);
return Result.success();
}
}
11.验证码Controller类
@RestController
@RequestMapping("/code")
public class CodeController {
@Autowired
private VerificationCodeService codeService;
@ApiOperation("请求权限码接口")
@PostMapping("/getCode")
public Result getRequestPermissionCode(@RequestBody FindPasswordInfo info) {
return codeService.getRequestPermissionCode(info);
}
@ApiOperation("发送邮箱验证码接口")
@PostMapping("/sendEmailCode")
public Result sendEmailCode(@RequestBody FindPasswordInfo info) {
return codeService.sendEmailCode(info);
}
}
12.找回密码服务类
public interface LoginService {
/**
* 找回密码
* @param info (邮箱、验证码、新密码、确认密码)
* @return
*/
Result findPassword(FindPasswordInfo info);
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private IUserService userService;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result findPassword(FindPasswordInfo info) {
if (info == null)
return Result.error("参数格式不规范");
//获取参数
String email = info.getEmail();
String code = info.getCode();
String password = info.getPassword();
String passwordConfirm = info.getPasswordConfirm();
if (StringUtils.isBlank(email) && StringUtils.isBlank(code) && StringUtils.isBlank(password) && StringUtils.isBlank(passwordConfirm)){
return Result.error("参数不合法");
}
else if (!StringUtil.checkEmail(email)) {
// 邮箱格式校验
return Result.error("邮箱格式不正确");
}
else if (password.length() !=6) {
// 密码格式校验
return Result.error("密码格式不正确");
}
else if (code.length() != 6) {
// 验证码长度校验
return Result.error("验证码格式不正确");
}
else if (!passwordConfirm.equals(password)) {
// 密码与确认密码校验
return Result.error("两次输入密码不相同");
}
// 构造查询条件对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "secret_key");
wrapper.eq("email", email);
wrapper.last("limit 1");
// 查询用户,是否存在
User user = userService.getOne(wrapper);
if (user == null) {
return Result.error("该用户不存在");
}
// 获取正确的验证码
String rightCode = redisTemplate.opsForValue().get("EMAIL_" + email);
if (!code.equals(rightCode)) {
// 验证码比对
return Result.error("验证码错误");
}
// 删除验证码
redisTemplate.delete("EMAIL_" + email);
// 修改密码
User user1 = new User();
user1.setId(user.getId());
//对密码进行加密
String pwd = SaltUtil.createCredentials(password,user.getSecretKey());
user1.setPassword(pwd);
// 修改
return this.userService.updateById(user1) == false ? Result.error("修改失败,请联系管理员") : Result.success();
}
}
13.找回密码Controller类
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@ApiOperation("找回密码接口")
@PostMapping("/findPassword")
public Result findPassword(@RequestBody FindPasswordInfo info){
return loginService.findPassword(info);
}
}
结束语
感谢大家的观看,希望对大家有帮助,有问题可以指出!