实体类
验证码pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("verification_code")
public class VerificationCode implements Serializable {
@TableId
//@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 使用场景,自己定义 */
private String scenes;
/** true 为有效,false 为无效,验证时状态+时间+具体的邮箱或者手机号 */
private Boolean status = true;
/** 类型 :phone 和 email */
private String type;
/** 具体的phone与email */
private String value;
/** 创建日期 */
@TableField(fill = FieldFill.INSERT)
// @Column(name = "create_time")
private Timestamp createTime;
public VerificationCode(String code, String scenes, @NotBlank String type, @NotBlank String value) {
this.code = code;
this.scenes = scenes;
this.type = type;
this.value = value;
}
}
邮箱配置pojo:
@Data
@TableName("email_config")
public class EmailConfig implements Serializable {
/** ID */
@TableId
//@GeneratedValue(strategy = GenerationType.IDENTITY)
// @Column(name = "id")
private Long id;
/** 收件人 */
// @Column(name = "from_user")
private String fromUser;
/** 邮件服务器SMTP地址 */
// @Column(name = "host")
private String host;
/** 密码 */
// @Column(name = "pass")
private String pass;
/** 端口 */
// @Column(name = "port")
private String port;
/** 发件者用户名 */
// @Column(name = "user")
private String user;
public void copy(EmailConfig source) {
BeanUtil.copyProperties(source, this, CopyOptions.create().setIgnoreNullValue(true));
}
}
sql:
DROP TABLE IF EXISTS `verification_code`;
CREATE TABLE `verification_code` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '验证码',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`status` bit(1) DEFAULT NULL COMMENT '状态:1有效、0过期',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '验证码类型:email或者短信',
`value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '接收邮箱或者手机号码',
`scenes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '业务名称:如重置邮箱、重置密码等',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='验证码';
DROP TABLE IF EXISTS `email_config`;
CREATE TABLE `email_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`from_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '收件人',
`host` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '邮件服务器SMTP地址',
`pass` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密码',
`port` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '端口',
`user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '发件者用户名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='邮箱配置';
邮箱vo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmailVo {
/** 收件人,支持多个收件人 */
@NotEmpty
private List<String> tos;
@NotBlank
private String subject;
@NotBlank
private String content;
}
controller:
@RestController
@RequestMapping("/api/code")
@Api(tags = "工具:验证码管理")
public class VerificationCodeController {
@PostMapping(value = "/resetEmail")
@ApiOperation("重置邮箱,发送验证码")
public ResponseEntity<Object> resetEmail(@RequestBody VerificationCode code) throws Exception {
code.setScenes(YshopConstant.RESET_MAIL);
EmailVo emailVo = verificationCodeService.sendEmail(code);
emailService.send(emailVo, emailService.find());
return new ResponseEntity<>(HttpStatus.OK);
}
前端页面(Vue)
<template>
<div style="display: inline-block;">
<el-dialog :visible.sync="dialog" :close-on-click-modal="false" :before-close="cancel" :title="title" append-to-body width="475px" @close="cancel">
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="88px">
<el-form-item label="新邮箱" prop="email">
<el-input v-model="form.email" auto-complete="on" style="width: 200px;" />
<el-button :loading="codeLoading" :disabled="isDisabled" size="small" @click="sendCode">{
{
buttonName }}</el-button>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-input v-model="form.code" style="width: 320px;" />
</el-form-item>
<el-form-item label="当前密码" prop="pass">
<el-input v-model="form.pass" type="password" style="width: 320px;" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="text" @click="cancel">取消</el-button>
<el-button :loading="loading" type="primary" @click="doSubmit">确认</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import store from '@/store'
import {
validEmail } from '@/utils/validate'
import {
updateEmail } from '@/api/system/user'
import {
resetEmail } from '@/api/system/code'
export default {
props: {
email: {
type: String,
required: true
}
},
data() {
const validMail = (rule, value, callback) => {
if (value === '' || value === null) {
callback(new Error('新邮箱不能为空'))
} else if (value === this.email) {
callback(new Error('新邮箱不能与旧邮箱相同'))
} else if (validEmail(value)) {
callback()
} else {
callback(new Error('邮箱格式错误'))
}
}
return {
loading: false, dialog: false, title: '修改邮箱', form: {
pass: '', email: '', code: '' },
user: {
email: '', password: '' }, codeLoading: false,
codeData: {
type: 'email', value: '' },
buttonName: '获取验证码', isDisabled: false, time: 60,
rules: {
pass: [
{
required: true, message: '当前密码不能为空', trigger: 'blur' }
],
email: [
{
required: true, validator: validMail, trigger: 'blur' }
],
code: [
{
required: true, message: '验证码不能为空', trigger: 'blur' }
]
}
}
},
methods: {
cancel() {
this.resetForm()
},
sendCode() {
if (this.form.email && this.form.email !== this.email) {
this.codeLoading = true
this.buttonName = '验证码发送中'
this.codeData.value = this.form.email
const _this = this
resetEmail(this.codeData).then(res => {
this.$message({
showClose: true,
message: '发送成功,验证码有效期5分钟',
type: 'success'
})
this.codeLoading = false
this.isDisabled = true
this.buttonName = this.time-- + '秒后重新发送'
this.timer = window.setInterval(function() {
_this.buttonName = _this.time + '秒后重新发送'
--_this.time
if (_this.time < 0) {
_this.buttonName = '重新发送'
_this.time = 60
_this.isDisabled = false
window.clearInterval(_this.timer)
}
}, 1000)
}).catch(err => {
this.resetForm()
this.codeLoading = false
console.log(err.response.data.message)
})
}
},
doSubmit() {
this.$refs['form'].validate((valid) => {
if (valid) {
this.loading = true
updateEmail(this.form).then(res => {
this.loading = false
this.resetForm()
this.$notify({
title: '邮箱修改成功',
type: 'success',
duration: 1500
})
store.dispatch('GetInfo').then(() => {
})
}).catch(err => {
this.loading = false
console.log(err.response.data.message)
})
} else {
return false
}
})
},
resetForm() {
this.dialog = false
this.$refs['form'].resetFields()
window.clearInterval(this.timer)
this.time = 60
this.buttonName = '获取验证码'
this.isDisabled = false
this.form = {
pass: '', email: '', code: '' }
}
}
}
</script>
<style scoped>
</style>
js:
import request from '@/utils/request'
export function resetEmail(data) {
return request({
url: 'api/code/resetEmail',
method: 'post',
data
})
}
export function updatePass(pass) {
return request({
url: 'api/users/updatePass/' + pass,
method: 'get'
})
}
生成校验验证码service:
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import co.yixiang.common.service.impl.BaseServiceImpl;
import co.yixiang.exception.BadRequestException;
import co.yixiang.tools.domain.VerificationCode;
import co.yixiang.tools.domain.vo.EmailVo;
import co.yixiang.tools.service.VerificationCodeService;
import co.yixiang.tools.service.mapper.VerificationCodeMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class VerificationCodeServiceImpl extends BaseServiceImpl<VerificationCodeMapper, VerificationCode> implements VerificationCodeService {
@Value("${code.expiration}")
private Integer expiration;
@Override
@Transactional(rollbackFor = Exception.class)
public EmailVo sendEmail(VerificationCode code) {
EmailVo emailVo;
String content;
VerificationCode verificationCode = this.getOne(new LambdaQueryWrapper<VerificationCode>()
.eq(VerificationCode::getScenes, code.getScenes()).eq(VerificationCode::getType, code.getType()).eq(VerificationCode::getValue, code.getValue()));
// 如果不存在有效的验证码,就创建一个新的
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate("email/email.ftl");
if (verificationCode == null) {
code.setCode(RandomUtil.randomNumbers(6));//package cn.hutool.core.util包下的RandomUtil工具
content = template.render(Dict.create().set("code", code.getCode()));
emailVo = new EmailVo(Collections.singletonList(code.getValue()), "yxiang后台管理系统", content);
this.save(code);
timedDestruction(code);
// 存在就再次发送原来的验证码
} else {
content = template.render(Dict.create().set("code", verificationCode.getCode()));
emailVo = new EmailVo(Collections.singletonList(verificationCode.getValue()), "yshop后台管理系统", content);
}
return emailVo;
}
@Override
public void validated(VerificationCode code) {
VerificationCode verificationCode = this.getOne(new LambdaQueryWrapper<VerificationCode>()
.eq(VerificationCode::getScenes, code.getScenes()).eq(VerificationCode::getType, code.getType()).eq(VerificationCode::getValue, code.getValue())
.eq(VerificationCode::getStatus, true));
if (verificationCode == null || !verificationCode.getCode().equals(code.getCode())) {
throw new BadRequestException("无效验证码");
} else {
verificationCode.setStatus(false);
this.save(verificationCode);
}
}
/**
* 定时任务,指定分钟后改变验证码状态
* @param verifyCode 验证码
*/
private void timedDestruction(VerificationCode verifyCode) {
//以下示例为程序调用结束继续运行
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("verifyCode-schedule-pool-%d").daemon(true).build());
try {
executorService.schedule(() -> {
verifyCode.setStatus(false);
this.save(verifyCode);
}, expiration * 60 * 1000L, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
}
ftl模板:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
@page {
margin: 0;
}
</style>
</head>
<body style="margin: 0px;
padding: 0px;
font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif;
color: #000;">
<div style="height: auto;
width: 820px;
min-width: 820px;
margin: 0 auto;
margin-top: 20px;
border: 1px solid #eee;">
<div style="padding: 10px;padding-bottom: 0px;">
<p style="margin-bottom: 10px;padding-bottom: 0px;">尊敬的用户,您好:</p>
<p style="text-indent: 2em; margin-bottom: 10px;">您正在申请邮箱验证,您的验证码为:</p>
<p style="text-align: center;
font-family: Times New Roman;
font-size: 22px;
color: #C60024;
padding: 20px 0px;
margin-bottom: 10px;
font-weight: bold;
background: #ebebeb;">${code}</p>
<div class="foot-hr hr" style="margin: 0 auto;
z-index: 111;
width: 800px;
margin-top: 30px;
border-top: 1px solid #DA251D;">
</div>
</div>
</div>
</body>
</html>
真正发送邮箱的EmailConfigService
public interface EmailConfigService extends BaseService<EmailConfig> {
/**
* 更新邮件配置
* @param emailConfig 邮件配置
* @param old 旧的配置
* @return EmailConfig
*/
void update(EmailConfig emailConfig, EmailConfig old);
/**
* 查询配置
* @return EmailConfig 邮件配置
*/
EmailConfig find();
/**
* 发送邮件
* @param emailVo 邮件发送的内容
* @param emailConfig 邮件配置
* @throws Exception /
*/
@Async
void send(EmailVo emailVo, EmailConfig emailConfig) throws Exception;
}
import cn.hutool.extra.mail.Mail;
import cn.hutool.extra.mail.MailAccount;
@Service
@AllArgsConstructor
//@CacheConfig(cacheNames = "emailConfig")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class EmailConfigServiceImpl extends BaseServiceImpl<EmailConfigMapper, EmailConfig> implements EmailConfigService {
private final IGenerator generator;
@Override
// @CachePut(key = "'1'")
@Transactional(rollbackFor = Exception.class)
public void update(EmailConfig emailConfig, EmailConfig old) {
try {
if (!emailConfig.getPass().equals(old.getPass())) {
// 对称加密
emailConfig.setPass(EncryptUtils.desEncrypt(emailConfig.getPass()));
}
} catch (Exception e) {
e.printStackTrace();
}
this.save(emailConfig);
}
@Override
// @Cacheable(key = "'1'")
public EmailConfig find() {
EmailConfig emailConfig = this.list().get(0);
return emailConfig;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void send(EmailVo emailVo, EmailConfig emailConfig) {
if (emailConfig == null) {
throw new BadRequestException("请先配置,再操作");
}
// 封装
MailAccount account = new MailAccount();
account.setHost(emailConfig.getHost());
account.setPort(Integer.parseInt(emailConfig.getPort()));
account.setAuth(true);
try {
// 对称解密
account.setPass(EncryptUtils.desDecrypt(emailConfig.getPass()));
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
account.setFrom(emailConfig.getUser() + "<" + emailConfig.getFromUser() + ">");
// ssl方式发送
account.setSslEnable(true);
// 使用STARTTLS安全连接
account.setStarttlsEnable(true);
String content = emailVo.getContent();
// 发送
try {
int size = emailVo.getTos().size();
Mail.create(account)
.setTos(emailVo.getTos().toArray(new String[size]))
.setTitle(emailVo.getSubject())
.setContent(content)
.setHtml(true)
//关闭session
.setUseGlobalSession(false)
.send();
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
}