新增:新增修改邮箱功能,并优化部分以往代码(引入 spring-boot-starter-mail 用于发送邮件验证码)

This commit is contained in:
2023-01-14 01:05:39 +08:00
parent 73fadb8315
commit 8b82557883
45 changed files with 1318 additions and 280 deletions

View File

@@ -1,72 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.webapi.controller.auth;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wf.captcha.base.Captcha;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.util.IdUtil;
import top.charles7c.cnadmin.auth.config.properties.CaptchaProperties;
import top.charles7c.cnadmin.auth.model.vo.CaptchaVO;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.RedisUtils;
/**
* 验证码 API
*
* @author Charles7c
* @since 2022/12/11 14:00
*/
@Tag(name = "验证码 API")
@SaIgnore
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/captcha", produces = MediaType.APPLICATION_JSON_VALUE)
public class CaptchaController {
private final CaptchaProperties captchaProperties;
@Operation(summary = "获取图片验证码", description = "获取图片验证码Base64编码带图片格式data:image/gif;base64")
@GetMapping("/img")
public R<CaptchaVO> getImageCaptcha() {
// 生成验证码
Captcha captcha = captchaProperties.getCaptcha();
// 保存验证码
String uuid = IdUtil.fastSimpleUUID();
String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), uuid);
RedisUtils.setCacheObject(captchaKey, captcha.text(),
Duration.ofMinutes(captchaProperties.getExpirationInMinutes()));
// 返回验证码
CaptchaVO captchaVo = new CaptchaVO().setUuid(uuid).setImg(captcha.toBase64());
return R.ok(captchaVo);
}
}

View File

@@ -31,11 +31,12 @@ import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import top.charles7c.cnadmin.auth.config.properties.CaptchaProperties;
import top.charles7c.cnadmin.auth.model.request.LoginRequest;
import top.charles7c.cnadmin.auth.model.vo.LoginVO;
import top.charles7c.cnadmin.auth.model.vo.UserInfoVO;
import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.common.config.properties.CaptchaProperties;
import top.charles7c.cnadmin.common.consts.CacheConstants;
import top.charles7c.cnadmin.common.model.dto.LoginUser;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.ExceptionUtils;
@@ -64,11 +65,11 @@ public class LoginController {
@PostMapping("/login")
public R<LoginVO> login(@Validated @RequestBody LoginRequest loginRequest) {
// 校验验证码
String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), loginRequest.getUuid());
String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, loginRequest.getUuid());
String captcha = RedisUtils.getCacheObject(captchaKey);
ValidationUtils.exIfBlank(captcha, "验证码已失效");
RedisUtils.deleteCacheObject(captchaKey);
ValidationUtils.exIfCondition(() -> !captcha.equalsIgnoreCase(loginRequest.getCaptcha()), "验证码错误");
ValidationUtils.exIfNotEqualIgnoreCase(loginRequest.getCaptcha(), captcha, "验证码错误");
// 用户登录
String rawPassword =
@@ -84,7 +85,6 @@ public class LoginController {
in = ParameterIn.HEADER)
@PostMapping("/logout")
public R logout() {
ValidationUtils.exIfCondition(() -> !StpUtil.isLogin(), "Token 无效");
StpUtil.logout();
return R.ok();
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.webapi.controller.common;
import java.time.Duration;
import javax.mail.MessagingException;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wf.captcha.base.Captcha;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.RegexPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import top.charles7c.cnadmin.common.config.properties.CaptchaProperties;
import top.charles7c.cnadmin.common.config.properties.ContinewAdminProperties;
import top.charles7c.cnadmin.common.consts.CacheConstants;
import top.charles7c.cnadmin.common.model.vo.CaptchaVO;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.*;
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
/**
* 验证码 API
*
* @author Charles7c
* @since 2022/12/11 14:00
*/
@Tag(name = "验证码 API")
@SaIgnore
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/common/captcha", produces = MediaType.APPLICATION_JSON_VALUE)
public class CaptchaController {
private final CaptchaProperties captchaProperties;
private final ContinewAdminProperties properties;
@Operation(summary = "获取图片验证码", description = "获取图片验证码Base64编码带图片格式data:image/gif;base64")
@GetMapping("/img")
public R<CaptchaVO> getImageCaptcha() {
// 生成验证码
CaptchaProperties.CaptchaImage captchaImage = captchaProperties.getImage();
Captcha captcha = captchaImage.getCaptcha();
// 保存验证码
String uuid = IdUtil.fastSimpleUUID();
String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, uuid);
RedisUtils.setCacheObject(captchaKey, captcha.text(),
Duration.ofMinutes(captchaImage.getExpirationInMinutes()));
// 返回验证码
CaptchaVO captchaVo = new CaptchaVO().setUuid(uuid).setImg(captcha.toBase64());
return R.ok(captchaVo);
}
@Operation(summary = "获取邮箱验证码", description = "发送验证码到指定邮箱")
@GetMapping("/mail")
public R getMailCaptcha(
@NotBlank(message = "邮箱不能为空") @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") String email)
throws MessagingException {
// 校验
String limitCacheKey = CacheConstants.LIMIT_CACHE_KEY;
String captchaCacheKey = CacheConstants.CAPTCHA_CACHE_KEY;
String limitCaptchaKey = RedisUtils.formatKey(limitCacheKey, captchaCacheKey, email);
long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey);
ValidationUtils.exIfCondition(() -> limitTimeInMillisecond > 0,
String.format("发送邮箱验证码过于频繁,请您 %ds 后再试", limitTimeInMillisecond / 1000));
// 生成验证码
CaptchaProperties.CaptchaMail captchaMail = captchaProperties.getMail();
String captcha = RandomUtil.randomNumbers(captchaMail.getLength());
// 发送验证码
Long expirationInMinutes = captchaMail.getExpirationInMinutes();
String content = TemplateUtils.render(captchaMail.getTemplatePath(),
Dict.create().set("captcha", captcha).set("expiration", expirationInMinutes));
MailUtils.sendHtml(email, String.format("【%s】邮箱验证码", properties.getName()), content);
// 保存验证码
String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, email);
RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes));
RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaMail.getLimitInSeconds()));
return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes));
}
}

View File

@@ -34,15 +34,18 @@ import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import top.charles7c.cnadmin.common.config.properties.LocalStorageProperties;
import top.charles7c.cnadmin.common.consts.CacheConstants;
import top.charles7c.cnadmin.common.consts.FileConstants;
import top.charles7c.cnadmin.common.consts.RegExpConstants;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.ExceptionUtils;
import top.charles7c.cnadmin.common.util.RedisUtils;
import top.charles7c.cnadmin.common.util.SecureUtils;
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
import top.charles7c.cnadmin.system.model.entity.SysUser;
import top.charles7c.cnadmin.system.model.request.UpdateBasicInfoRequest;
import top.charles7c.cnadmin.system.model.request.UpdateEmailRequest;
import top.charles7c.cnadmin.system.model.request.UpdatePasswordRequest;
import top.charles7c.cnadmin.system.model.vo.AvatarVO;
import top.charles7c.cnadmin.system.service.UserService;
@@ -111,4 +114,24 @@ public class UserCenterController {
userService.updatePassword(rawOldPassword, rawNewPassword, LoginHelper.getUserId());
return R.ok("修改成功");
}
@Operation(summary = "修改邮箱", description = "修改用户邮箱")
@PatchMapping("/email")
public R updateEmail(@Validated @RequestBody UpdateEmailRequest updateEmailRequest) {
// 解密
String rawCurrentPassword =
ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateEmailRequest.getCurrentPassword()));
ValidationUtils.exIfBlank(rawCurrentPassword, "当前密码解密失败");
// 校验
String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, updateEmailRequest.getNewEmail());
String captcha = RedisUtils.getCacheObject(captchaKey);
ValidationUtils.exIfBlank(captcha, "验证码已失效");
ValidationUtils.exIfNotEqualIgnoreCase(updateEmailRequest.getCaptcha(), captcha, "验证码错误");
RedisUtils.deleteCacheObject(captchaKey);
// 修改邮箱
userService.updateEmail(updateEmailRequest.getNewEmail(), rawCurrentPassword, LoginHelper.getUserId());
return R.ok("修改成功");
}
}

View File

@@ -69,6 +69,48 @@ spring:
# 是否开启 SSL
ssl: false
--- ### 邮件配置
spring:
mail:
# 根据需要更换
host: smtp.126.com
port: 465
username: 你的邮箱
password: 你的邮箱授权码
default-encoding: utf-8
properties:
mail:
smtp:
auth: true
socketFactory:
class: javax.net.ssl.SSLSocketFactory
port: 465
--- ### 验证码配置
captcha:
## 图片验证码配置
image:
# 类型
type: SPEC
# 内容长度
length: 4
# 过期时间
expirationInMinutes: 2
# 宽度
width: 111
# 高度
height: 36
## 邮箱验证码配置
mail:
# 内容长度
length: 6
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
--- ### 安全配置
security:
# 排除路径配置
@@ -95,21 +137,6 @@ rsa:
# 私钥
privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV
--- ### 验证码配置
captcha:
# 类型
type: SPEC
# 缓存键的前缀
keyPrefix: CAPTCHA
# 过期时间
expirationInMinutes: 2
# 内容长度
length: 4
# 宽度
width: 111
# 高度
height: 36
--- ### 接口文档配置
springdoc:
swagger-ui:

View File

@@ -69,6 +69,48 @@ spring:
# 是否开启 SSL
ssl: false
--- ### 邮件配置
spring:
mail:
# 根据需要更换
host: smtp.126.com
port: 465
username: 你的邮箱
password: 你的邮箱授权码
default-encoding: utf-8
properties:
mail:
smtp:
auth: true
socketFactory:
class: javax.net.ssl.SSLSocketFactory
port: 465
--- ### 验证码配置
captcha:
## 图片验证码配置
image:
# 类型
type: SPEC
# 内容长度
length: 4
# 过期时间
expirationInMinutes: 2
# 宽度
width: 111
# 高度
height: 36
## 邮箱验证码配置
mail:
# 内容长度
length: 6
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
--- ### 安全配置
security:
# 排除路径配置
@@ -88,21 +130,6 @@ rsa:
# 私钥
privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV
--- ### 验证码配置
captcha:
# 类型
type: SPEC
# 缓存键的前缀
keyPrefix: CAPTCHA
# 过期时间
expirationInMinutes: 2
# 内容长度
length: 4
# 宽度
width: 111
# 高度
height: 36
--- ### 接口文档配置
springdoc:
swagger-ui:

View File

@@ -1,13 +1,13 @@
--- ### 项目配置
continew-admin:
# 名称
name: ContiNew-Admin
name: ContiNew Admin
# 应用名称
appName: continew-admin
# 版本
version: 0.0.1-SNAPSHOT
# 描述
description: ContiNew-Admin (incubating) 中后台管理框架Continue New Admin持续以最新流行技术栈构建。
description: ContiNew Admin 中后台管理框架(孵化中)Continue New Admin持续以最新流行技术栈构建。
# URL
url: https://github.com/Charles7c/continew-admin
## 作者信息配置
@@ -65,7 +65,7 @@ knife4j:
# 是否自定义 footer默认 false 非自定义)
enable-footer-custom: true
# 自定义 footer 内容,支持 Markdown 语法
footer-custom-content: '[Apache-2.0](https://github.com/Charles7c/continew-admin/blob/dev/LICENSE) | Copyright © 2022-present [ContiNew-Admin](https://github.com/Charles7c/continew-admin)'
footer-custom-content: 'Copyright © 2022-present Charles7c&nbsp;⋅&nbsp;[ContiNew Admin](https://github.com/Charles7c/continew-admin)'
--- ### Sa-Token 配置
sa-token:

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="邮箱验证码">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base target="_blank">
<style>::-webkit-scrollbar{ display: none; }</style>
</head>
<body tabindex="0">
<div style="background-color: #ECECEC; padding: 25px;">
<div style="margin: 0 auto; text-align: left; position: relative; border-radius: 5px; border-collapse: collapse; box-shadow: rgb(153, 153, 153) 0px 0px 5px; background: #fff; font-family: 微软雅黑, 黑体, sans-serif; font-size: 14px; line-height: 1.5;">
<div style="height: 29px; line-height: 25px; padding: 15px 30px; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #307AF2; background: #00308f; border-radius: 5px 5px 0 0;">
<div style="font-size: 24px; font-weight: bolder; color: #fff; display: inline-flex; align-items: center;">
<a href="https://cnadmin.charles7c.top/">
<img src="https://cnadmin.charles7c.top/logo.svg" alt="ContiNew Admin" style="vertical-align: middle;">
</a>
<a href="https://cnadmin.charles7c.top/" style="margin-left: 4px; text-decoration: none; color: #fff;">ContiNew Admin</a>
</div>
</div>
<div style="word-break: break-word;">
<div style="border-radius: 5px; padding: 25px 30px 11px; background-color: #fff; opacity: 0.8;">
<h2 style="margin: 5px 0; font-size: 18px; line-height: 22px; color: #333;">亲爱的用户:</h2>
<p>
您好!感谢您使用 <a href="https://github.com/Charles7c/continew-admin" style="color: #333;">ContiNew Admin</a>,本次请求的验证码为:<span style="font-size: 16px; color: #ff8c00;">${captcha}</span>,请在 ${expiration} 分钟内使用此验证码完成验证。
</p>
<br>
<h2 style="margin: 5px 0; font-size: 18px; line-height: 22px; color: #333;">Dear user:</h2>
<p>
Hello! Thanks for using ContiNew Admin, The verification code for this request is:&nbsp;<span style="font-size: 16px; color: #ff8c00;">${captcha}</span>, please use this verification code to complete the verification within ${expiration} minutes.
</p>
<div style="width: 100%; margin: 0 auto;">
<div style="padding: 10px 10px 0; border-top: 1px solid #ccc; color: #747474; margin-bottom: 20px; line-height: 1.3em; font-size: 12px;">
<p>
若非本人操作,请忽略此邮件。此邮件由系统自动发送,请勿直接回复该邮件。<br>
Please ignore this email if not by yourself. This email is sent automatically by the system, please do not reply to this email directly.
</p>
<p>Copyright © 2022-present Charles7c</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>