Springboot+Vue实现短信与邮箱验证码登录
体验网址:https://mxyit.com
示例
1、新增依赖
<!-- 短信服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.1.0</version>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2、前端示例
Login.vue
<el-tabs v-show="!isRegist" type="border-card" class="login-tab">
<el-tab-pane><span slot="label"><i class="el-icon-mobile-phone"></i> 手机号登录</span>
<el-form
ref="phoneForm"
:model="phoneForm"
:rules="loginRules"
autocomplete="off"
:hide-required-asterisk="true"
size="medium"
>
<el-form-item prop="phoneNo">
<span class="svg-container">
<svg-icon icon-class="user"/>
</span>
<el-input
v-model="phoneForm.phoneNo"
placeholder="请输入手机号/自动注册"
/>
</el-form-item>
<div>
<div style="width: 50%;display: inline-block;">
<el-form-item prop="code">
<span class="svg-container">
<svg-icon icon-class="password"/>
</span>
<el-input
v-model="phoneForm.code"
maxlength="6"
placeholder="请输入验证码"
/>
</el-form-item>
</div>
<div style="display: inline-block;margin-left: 20px;">
<el-button
:loading="codeLoading"
:disabled="isDisable"
size="small"
round
@click="sendMsg(1)"
>短信验证
</el-button>
<span class="status">{{ statusMsg }}</span>
</div>
</div>
<el-button
:loading="loading"
type="primary"
style="width:100%;margin-bottom:15px;"
@click.native.prevent="phoneLogin"
>登录/注册
</el-button>
<p class="tips">
<a @click.prevent="isRegist=true" type="primary">账号密码登录</a>
</p>
</el-form>
</el-tab-pane>
<el-tab-pane><span slot="label"><i class="el-icon-mobile-phone"></i> 邮箱登录</span>
<el-form
ref="emailForm"
:model="emailForm"
:rules="loginRules"
autocomplete="off"
:hide-required-asterisk="true"
size="medium"
>
<el-form-item prop="email">
<span class="svg-container">
<svg-icon icon-class="user"/>
</span>
<el-input
v-model="emailForm.email"
placeholder="请输入邮箱/自动注册"
/>
</el-form-item>
<div>
<div style="width: 50%;display: inline-block;">
<el-form-item prop="code">
<span class="svg-container">
<svg-icon icon-class="password"/>
</span>
<el-input
v-model="emailForm.code"
maxlength="6"
placeholder="请输入验证码"
/>
</el-form-item>
</div>
<div style="display: inline-block;margin-left: 20px;">
<el-button
:loading="codeLoading"
:disabled="isDisable"
size="small"
round
@click="sendMsg(2)"
>邮箱验证
</el-button>
<span class="status">{{ statusMsg }}</span>
</div>
</div>
<el-button
:loading="loading"
type="primary"
style="width:100%;margin-bottom:15px;"
@click.native.prevent="emailLogin"
>登录/注册
</el-button>
<p class="tips">
<a @click.prevent="isRegist=true" type="primary">账号密码登录</a>
</p>
</el-form>
</el-tab-pane>
<div style="height: 70px;">
<el-divider><span style="color: #8c92a4">其它方式登录</span></el-divider>
<social-sign/>
</div>
</el-tabs>
Login.vue
<script>
import axios from 'axios'
export default {
name: 'Login',
data() {
const validateUsername = (rule, value, callback) => {
if (value.length === 0) {
callback(new Error('请输入正确的用户名'))
} else {
callback()
}
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('密码不能小于6位'))
} else {
callback()
}
}
const validatePhone = (rule, value, callback) => {
const reg = /^[1][3-9][0-9]{9}$/;
if (value == '' || value == undefined || value == null) {
callback(new Error('请输入手机号码'));
} else {
if ((!reg.test(value)) && value != '') {
callback(new Error('请输入正确的手机号码'));
} else {
callback();
}
}
}
return {
loginForm: {
username: '',
password: '',
key: '',
captcha: ''
},
phoneForm: {
phoneNo: '',
code: ''
},
emailForm: {
email: '',
code: ''
},
loginRules: {
username: [{required: true, trigger: 'blur', validator: validateUsername}],
password: [{required: true, trigger: 'blur', validator: validatePassword}],
phoneNo: [{required: true, trigger: 'blur', validator: validatePhone}],
email: [{
required: true,
type: 'email',
message: '请输入邮箱',
trigger: 'blur'
}],
captcha: [{
required: true,
type: 'string',
message: '请输入验证码',
trigger: 'blur'
}],
code: [{
required: true,
type: 'string',
message: '请输入验证码',
trigger: 'blur'
}],
pwd: [{
required: true,
message: '创建密码',
trigger: 'blur'
}, {pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$/, message: '密码必须同时包含数字与字母,且长度为 8-20位'}],
cpwd: [{
required: true,
message: '确认密码',
trigger: 'blur'
}, {
validator: (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.ruleForm.pwd) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}]
},
statusMsg: '',
loading: false,
isDisable: false,// 验证码按钮禁用
codeLoading: false,// 验证码loading
passwordType: 'password',
redirect: undefined,
isRegist: true,
imageSrc: '',
verKey: '',
showDialog: false
}
},
watch: {
$route: {
handler: function (route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
created() {
this.getCaptcha();
},
methods: {
/*获取验证码*/
getCaptcha() {
axios({
url: window.SITE_CONFIG['systemUrl'] + '/api/foreign/sms/get/captcha',
method: 'get',
params: {}
}).then(res => {
console.log(res)
let result = res.data.data
this.loginForm.key = result.key;
this.imageSrc = result.image;
}).catch(err => {
console.log(err)
})
},
sendMsg: function (type) {
if (type === 1) {
const self = this
let phonePass
let timeRid
if (timeRid) {
return false
}
self.statusMsg = ''
this.$refs['phoneForm'].validateField('phoneNo', (valid) => {
phonePass = valid
})
// 向后台API验证码发送
if (!phonePass) {
self.codeLoading = true
self.statusMsg = '验证码发送中...'
let url = "";
let params = {};
axios({
url: window.SITE_CONFIG['systemUrl'] + '/api/foreign/sms/customer/sendMessageCode',
method: 'get',
params: {phoneNo: self.phoneForm.phoneNo}
}).then(res => {
console.log(res)
let result = res.data
if (result.code == 200) {
this.$message({
showClose: true,
message: '发送成功,验证码有效期5分钟',
type: 'success'
})
let count = 60
self.phoneForm.code = ''
self.codeLoading = false
self.isDisable = true
self.statusMsg = `验证码已发送,${count--}秒后重新发送`
timeRid = window.setInterval(function () {
self.statusMsg = `验证码已发送,${count--}秒后重新发送`
if (count <= 0) {
window.clearInterval(timeRid)
self.isDisable = false
self.statusMsg = ''
}
}, 1000)
} else {
this.$message({
showClose: true,
message: result.data,
type: 'warning'
})
this.isDisable = false
this.statusMsg = ''
this.codeLoading = false
}
}).catch(err => {
console.log(err)
this.isDisable = false
this.statusMsg = ''
this.codeLoading = false
console.log(err.data)
})
}
}
if (type === 2) {
const self = this
let emailPass
let timeRid
if (timeRid) {
return false
}
self.statusMsg = ''
this.$refs['emailForm'].validateField('email', (valid) => {
emailPass = valid
})
// 向后台API验证码发送
if (!emailPass) {
self.codeLoading = true
self.statusMsg = '验证码发送中...'
axios({
url: window.SITE_CONFIG['systemUrl'] + '/api/foreign/sms/customer/sendEmailMessage',
method: 'get',
params: {email: self.emailForm.email}
}).then(res => {
console.log(res)
let result = res.data
if (result.code == 200) {
this.$message({
showClose: true,
message: '发送成功,验证码有效期5分钟',
type: 'success'
})
let count = 60
self.emailForm.code = ''
self.codeLoading = false
self.isDisable = true
self.statusMsg = `验证码已发送,${count--}秒后重新发送`
timeRid = window.setInterval(function () {
self.statusMsg = `验证码已发送,${count--}秒后重新发送`
if (count <= 0) {
window.clearInterval(timeRid)
self.isDisable = false
self.statusMsg = ''
}
}, 1000)
} else {
this.$message({
showClose: true,
message: result.data,
type: 'warning'
})
this.isDisable = false
this.statusMsg = ''
this.codeLoading = false
}
}).catch(err => {
console.log(err)
this.isDisable = false
this.statusMsg = ''
this.codeLoading = false
console.log(err.data)
})
}
}
},
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.show = false
this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({path: '/'})
this.loading = false
this.show = true
}).catch(() => {
this.loading = false
this.show = true
this.getCaptcha()
})
} else {
console.log('操作异常')
this.getCaptcha()
return false
}
})
},
phoneLogin() {
this.$refs.phoneForm.validate(valid => {
if (valid) {
this.loading = true
this.show = false
this.$store.dispatch('user/phoneLogin', this.phoneForm).then(() => {
this.$router.push({path: '/'})
this.loading = false
this.show = true
}).catch(() => {
this.loading = false
this.show = true
})
} else {
console.log('操作异常')
return false
}
})
},
emailLogin() {
this.$refs.emailForm.validate(valid => {
if (valid) {
this.loading = true
this.show = false
this.$store.dispatch('user/emailLogin', this.emailForm).then(() => {
this.$router.push({path: '/'})
this.loading = false
this.show = true
}).catch(() => {
this.loading = false
this.show = true
})
} else {
console.log('操作异常')
return false
}
})
}
}
}
</script>
3、后端示例
SmsController
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 短信验证码
*
* @author my
*/
@RestController
@CrossOrigin
@RequestMapping("/api/foreign/sms")
public class SmsController {
@Resource
private SmsService smsService;
/**
* 发送短信验证码
*
* @return
*/
@ApiOperation(value = "发送短信验证码", httpMethod = "GET", notes = "发送短信验证码")
@GetMapping(value = "/customer/sendMessageCode")
public String sendSmsMessage(@RequestParam(value = "phoneNo") String phoneNo) {
return smsService.sendSmsMessage(phoneNo);
}
/**
* 发送邮箱验证码
*
* @return
*/
@ApiOperation(value = "发送邮箱验证码", httpMethod = "GET", notes = "发送邮箱验证码")
@GetMapping(value = "/customer/sendEmailMessage")
public String sendEmailMessage(@RequestParam(value = "email") String email) {
return smsService.sendEmailMessage(email);
}
/**
* 获取验证码
*
* @return
*/
@ApiOperation(value = "获取验证码", httpMethod = "GET", notes = "获取验证码")
@GetMapping(value = "/get/captcha")
public String captcha() {
return smsService.captcha();
}
}
SmsService
import com.mxy.common.core.utils.DateUtils;
import com.mxy.common.core.utils.RedisUtil;
import com.mxy.common.core.utils.ServiceResult;
import com.wf.captcha.SpecCaptcha;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
@Service
public class SmsService {
@Value("${sms.max.limit}")
private Integer maxLimit;
@Value("${sms.sendError.content}")
private String sendErrorContent;
@Resource
private RedisUtil redisUtil;
@Resource
private SmsSend smsSend;
private static final String MESSAGE_LIMIT = "messager_limit:";
private static final String SEND_LIMIT = "sendmsg_limit_count:";
private static final String PHONE_NO = "message_phone_no:";
private static final String EMAIL_MESSAGE_LIMIT = "email_messager_limit:";
private static final String EMAIL_SEND_LIMIT = "email_sendmsg_limit_count:";
private static final String EMAIL_NO = "message_email_no:";
private static final String CAPTCHA_NO = "message_captcha_no:";
private Random random = new Random();
public String sendSmsMessage(String phoneNo) {
// 非空检验
if (StringUtils.isEmpty(phoneNo)) {
return ServiceResult.error("电话号码不能为空哦");
}
String sendMsgKey = SEND_LIMIT + phoneNo;
if (redisUtil.hasKey(sendMsgKey) && (int) redisUtil.get(sendMsgKey) > maxLimit) {
return ServiceResult.error(sendErrorContent);
}
String phoneNoKey = PHONE_NO + phoneNo;
// 如果验证码已发送 作废之前的验证码
if (redisUtil.hasKey(phoneNoKey)) {
redisUtil.del(phoneNoKey);
}
String limitKey = MESSAGE_LIMIT + phoneNo;
if (redisUtil.hasKey(limitKey)) {
return ServiceResult.error("一分钟只能请求一次哦");
}
// 随机生成6位验证码
String verifyCode = String.valueOf(random.nextInt(899999) + 100000);
// 限制60秒只能生成一次验证码
redisUtil.set(limitKey, verifyCode, 60);
// 短信验证码 5分钟失效
redisUtil.set(phoneNoKey, verifyCode, 300);
int count = (int) (redisUtil.get(sendMsgKey) == null ? 0 : redisUtil.get(sendMsgKey));
// 计算当天23点59分59秒的秒数
long expireTime = DateUtils.getDifferentTimes();
// 当日0点过期
redisUtil.set(sendMsgKey, count + 1, expireTime);
// 异步请求下发短信
smsSend.sendSmsMessage(phoneNo, verifyCode, 2);
return ServiceResult.success("短信发送成功");
}
public String sendEmailMessage(String email) {
// 非空检验
if (StringUtils.isEmpty(email)) {
return ServiceResult.error("邮箱地址不能为空哦");
}
String sendMsgKey = EMAIL_SEND_LIMIT + email;
if (redisUtil.hasKey(sendMsgKey) && (int) redisUtil.get(sendMsgKey) > maxLimit) {
return ServiceResult.error(sendErrorContent);
}
String emailNoKey = EMAIL_NO + email;
// 如果验证码已发送 作废之前的验证码
if (redisUtil.hasKey(emailNoKey)) {
redisUtil.del(emailNoKey);
}
String limitKey = EMAIL_MESSAGE_LIMIT + email;
if (redisUtil.hasKey(limitKey)) {
return ServiceResult.error("一分钟只能请求一次哦");
}
// 随机生成6位验证码
String verifyCode = String.valueOf(random.nextInt(899999) + 100000);
// 限制60秒只能生成一次验证码
redisUtil.set(limitKey, verifyCode, 60);
// 短信验证码 5分钟失效
redisUtil.set(emailNoKey, verifyCode, 300);
int count = (int) (redisUtil.get(sendMsgKey) == null ? 0 : redisUtil.get(sendMsgKey));
// 计算当天23点59分59秒的秒数
long expireTime = DateUtils.getDifferentTimes();
// 当日0点过期
redisUtil.set(sendMsgKey, count + 1, expireTime);
// 异步请求下发邮件
smsSend.sendEmailMessage(email, verifyCode);
return ServiceResult.success("邮件发送成功");
}
public String captcha() {
SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);
String verCode = specCaptcha.text().toLowerCase();
String key = UUID.randomUUID().toString();
// 存入redis并设置过期时间为300秒
redisUtil.set(CAPTCHA_NO + key, verCode, 300);
// 将key和base64返回给前端
Map<String, Object> map = new HashMap<>();
map.put("key", key);
map.put("image", specCaptcha.toBase64());
return ServiceResult.success(map);
}
}
SmsSend
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.profile.DefaultProfile;
import com.mxy.common.core.entity.SysSmsSendLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 短信发送
*
* @author my
*/
@Slf4j
@Component
public class SmsSend {
@Value("${sms.accessKeyId}")
private String accessKeyId;
@Value("${sms.secret}")
private String secret;
@Resource
private JavaMailSender sender;
/**
* @description: 短信验证码发送
* @param: phoneNo-接收号码、verifyCode-验证码、type-短信模板类型(默认通用模板)(1-注册、2-登录、0-通用)
* 签名说明:
* 个人日常博客
* 模板说明:
* 通用模板:SMS_25073XXXX 您的验证码${code},该验证码5分钟内有效,请勿泄漏于他人!
* 登录验证码模板:SMS_25073XXXX 验证码为:${code},您正在登录,若非本人操作,请勿泄露。
* 注册验证码模板:SMS_25075XXXX 您正在申请手机注册,验证码为:${code},5分钟内有效!
**/
@Async("threadPoolTaskExecutor")
public void sendSmsMessage(String phoneNo, String verifyCode, int type) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
accessKeyId, secret);
IAcsClient client = new DefaultAcsClient(profile);
// 组装请求对象
CommonRequest request = new CommonRequest();
// 短信API产品域名(接口地址固定,无需修改)
request.setDomain("dysmsapi.aliyuncs.com");
request.setVersion("2017-05-25");
request.setAction("SendSms");
request.putQueryParameter("RegionId", "cn-hangzhou");
// 接收号码
request.putQueryParameter("PhoneNumbers", phoneNo);
// 短信签名
request.putQueryParameter("SignName", "个人日常博客");
// 短信模板
String smsTemplate = "";
switch (type) {
case 1:
request.putQueryParameter("TemplateCode", "SMS_25075XXXX");
smsTemplate = "【个人日常博客】您正在申请手机注册,验证码为:" + verifyCode + ",5分钟内有效!";
break;
case 2:
request.putQueryParameter("TemplateCode", "SMS_25073XXXX");
smsTemplate = "【个人日常博客】验证码为:" + verifyCode + ",您正在登录,若非本人操作,请勿泄露。";
break;
default:
request.putQueryParameter("TemplateCode", "SMS_25073XXXX");
smsTemplate = "【个人日常博客】您的验证码" + verifyCode + ",该验证码5分钟内有效,请勿泄漏于他人!";
break;
}
// 验证码
request.putQueryParameter("TemplateParam", "{code:" + verifyCode + "}");
try {
CommonResponse response = client.getCommonResponse(request);
// 记录发送记录,可忽略
SysSmsSendLog sendLog = new SysSmsSendLog();
sendLog.setPhone(phoneNo);
sendLog.setRequest(smsTemplate);
sendLog.setResponse(String.valueOf(response.getData()));
sendLog.insert();
log.info(phoneNo + "短信发送:" + response.getData());
} catch (Exception e) {
e.printStackTrace();
log.info(phoneNo + "短信发送失败:" + e.getMessage());
}
}
@Async("threadPoolTaskExecutor")
public void sendEmailMessage(String email, String verifyCode) {
try {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setFrom("you.email@qq.com");
mailMessage.setTo(email);
mailMessage.setSubject("个人日常博客验证码");
mailMessage.setText("您的验证码" + verifyCode + ",该验证码5分钟内有效,请勿泄漏于他人!");
sender.send(mailMessage);
SysSmsSendLog sendLog = new SysSmsSendLog();
sendLog.setPhone(email);
sendLog.setRequest(mailMessage.getText());
sendLog.setResponse("OK");
log.info(email + "->邮件发送:" + mailMessage.getText());
sendLog.insert();
} catch (MailException e) {
e.printStackTrace();
log.info(email + "邮件发送失败:" + e.getMessage());
}
}
}
工具类
DateUtils
/**
* 计算当天23点59分59秒的秒数
*/
public static Long getDifferentTimes() {
//计算当前时间到0点的秒数
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 1);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.MILLISECOND, 0);
return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
}
RedisUtil
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*
* @author my
* @date 2022-02-10
*/
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
}
配置文件
application.yml
# 配置-email
spring:
mail:
host: smtp.qq.com # 发送邮件的服务器地址
username: you.email@qq.com # 开启 IMAP/SMTP服务 的qq邮箱的账号
password: xxxxxxxxxxxx # 开启 IMAP/SMTP服务 获得的授权码
default-encoding: UTF-8
# 阿里云短信秘钥
sms:
accessKeyId: xxxxxxxxxxxx
secret: xxxxxxxxxxxx
max:
limit: 10
sendError:
content: 当日的获取验证码次数已用尽,请联系管理员解除限制。