(相关资料图)
1、新增依赖
com.aliyun aliyun-java-sdk-core 4.1.0 org.springframework.boot spring-boot-starter-mail
2、前端示例
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;@Servicepublic 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 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@Componentpublic 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 */@Componentpublic final class RedisUtil { @Autowired private RedisTemplate 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
# 配置-emailspring: 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: 当日的获取验证码次数已用尽,请联系管理员解除限制。