提交 8b825578 编写于 作者: 查尔斯-BUG万象集's avatar 查尔斯-BUG万象集

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

上级 73fadb83
......@@ -49,6 +49,7 @@ jobs:
script: |
cd /docker
docker-compose up --force-recreate --build -d continew-admin-server
docker images | grep none | awk '{print $3}' | xargs docker rmi
# 部署前端
deploy-web:
......
# ContiNew-Admin 中后台管理框架
# ContiNew Admin 中后台管理框架
[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://github.com/Charles7c/continew-admin/blob/dev/LICENSE)
![SNAPSHOT](https://img.shields.io/badge/SNAPSHOT-v0.0.1-%23ff3f59.svg)
......@@ -7,7 +7,7 @@
## 简介
ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Vue3、TypeScript、Arco Design Pro Vue、Spring Boot、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Hutool 等。
ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Vue3、TypeScript、Arco Design Pro Vue、Spring Boot、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Hutool 等。
## 开始
......@@ -21,11 +21,11 @@ git clone https://github.com/Charles7c/continew-admin.git
# 2.在 IDE(IntelliJ IDEA/Eclipse)中打开本项目
# 3.修改配置文件中的 Redis 配置信息
# 3.修改配置文件中的数据源配置信息、Redis 配置信息、邮件配置信息等
# [3.也可以在 IntelliJ IDEA 中直接配置程序启动环境变量(DB_HOST、DB_PORT、DB_USER、DB_PWD、DB_NAME;REDIS_HOST、REDIS_PORT、REDIS_PWD、REDIS_DB)]
# 4.启动程序
# 4.1 启动成功:访问 http://localhost:8000/,页面输出:ContiNew-Admin backend service started successfully.
# 4.1 启动成功:访问 http://localhost:8000/,页面输出:ContiNew Admin backend service started successfully.
# 4.2 接口文档:http://localhost:8000/doc.html
# 5.部署
......@@ -72,7 +72,7 @@ yarn dev
| :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- |
| [Vue](https://cn.vuejs.org/) | 3.2.45 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 |
| [TypeScript](https://www.typescriptlang.org/zh/) | 4.9.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 |
| [Arco Design Pro Vue](http://pro.arco.design/) | 2.5.15 | 基于 Arco Design Vue 组件库的开箱即用的中后台前端解决方案。 |
| [Arco Design Pro Vue](http://pro.arco.design/) | 2.6.0 | 基于 Arco Design Vue 组件库的开箱即用的中后台前端解决方案。 |
| [Spring Boot](https://spring.io/projects/spring-boot) | 2.7.7 | 简化新 Spring 应用的初始搭建以及开发过程。 |
| [Undertow](https://undertow.io/) | 2.2.22.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 |
| [Sa-Token + JWT](https://sa-token.dev33.cn/) | 1.33.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 |
......@@ -110,10 +110,14 @@ continew-admin # 全局通用项目配置及依赖版本管理
│ │ ├─ webapi
│ │ │ └─ controller
│ │ │ ├─ auth # 认证相关 API
│ │ │ ├─ common # 公共相关 API(例如:验证码 API 等)
│ │ │ └─ system # 系统管理相关 API
│ │ └─ ContinewAdminApplication.java # 启动入口
│ └─ resources # 工程配置目录
│ └─ db.changelog.v0.0.1 # 数据库脚本文件
│ ├─ db.changelog # 数据库脚本文件
│ │ └─ v0.0.1 # v0.0.1 版本数据库脚本文件
│ └─ templates # 模板文件
│ └─ mail # 邮件模板
├─ continew-admin-monitor # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)
│ └─ src
│ └─ main
......@@ -144,8 +148,7 @@ continew-admin # 全局通用项目配置及依赖版本管理
│ │ └─ cnadmin
│ │ ├─ auth # 系统认证相关业务及配置
│ │ │ ├─ config # 系统认证相关配置
│ │ │ │ ├─ satoken # Sa-Token 配置
│ │ │ │ └─ properties # 系统认证相关配置属性
│ │ │ │ └─ satoken # Sa-Token 配置
│ │ │ ├─ model # 系统认证相关模型
│ │ │ │ ├─ request # 系统认证相关请求对象
│ │ │ │ └─ vo # 系统认证相关 VO(View Object)
......@@ -197,6 +200,7 @@ continew-admin
├─ src
│ ├─ api # 请求接口
│ │ ├─ auth # 认证模块
│ │ ├─ common # 公共模块
│ │ └─ system # 系统管理模块
│ ├─ assets # 静态资源
│ │ ├─ images # 图片资源
......
......@@ -59,6 +59,17 @@ limitations under the License.
</exclusions>
</dependency>
<!-- Java 邮件支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- FreeMarker(模板引擎) -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -126,5 +137,11 @@ limitations under the License.
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.cnadmin.auth.config.properties;
package top.charles7c.cnadmin.common.config.properties;
import java.awt.*;
......@@ -43,65 +43,102 @@ import cn.hutool.core.util.StrUtil;
public class CaptchaProperties {
/**
* 类型
* 图片验证码配置
*/
private CaptchaTypeEnum type;
private CaptchaImage image;
/**
* 缓存键的前缀
* 邮箱验证码配置
*/
private String keyPrefix;
private CaptchaMail mail;
/**
* 过期时间
* 图片验证码配置
*/
private Long expirationInMinutes = 2L;
@Data
public static class CaptchaImage {
/**
* 类型
*/
private CaptchaImageTypeEnum type;
/**
* 内容长度
*/
private int length = 4;
/**
* 内容长度
*/
private int length;
/**
* 宽度
*/
private int width = 111;
/**
* 过期时间
*/
private long expirationInMinutes;
/**
* 高
*/
private int height = 36;
/**
* 宽
*/
private int width = 111;
/**
* 字体
*/
private String fontName;
/**
* 高度
*/
private int height = 36;
/**
* 字体大小
*/
private int fontSize = 25;
/**
* 字体
*/
private String fontName;
/**
* 字体大小
*/
private int fontSize = 25;
/**
* 获取图片验证码对象
*
* @return 验证码对象
*/
public Captcha getCaptcha() {
Captcha captcha = ReflectUtil.newInstance(type.getClazz(), this.width, this.height);
captcha.setLen(length);
if (StrUtil.isNotBlank(this.fontName)) {
captcha.setFont(new Font(this.fontName, Font.PLAIN, this.fontSize));
}
return captcha;
}
}
/**
* 获取验证码对象
*
* @return 验证码对象
* 邮箱验证码配置
*/
public Captcha getCaptcha() {
Captcha captcha = ReflectUtil.newInstance(type.getClazz(), this.width, this.height);
captcha.setLen(length);
if (StrUtil.isNotBlank(this.fontName)) {
captcha.setFont(new Font(this.fontName, Font.PLAIN, this.fontSize));
}
return captcha;
@Data
public static class CaptchaMail {
/**
* 内容长度
*/
private int length;
/**
* 过期时间
*/
private long expirationInMinutes;
/**
* 限制时间
*/
private long limitInSeconds;
/**
* 模板路径
*/
private String templatePath;
}
/**
* 验证码类型枚举
* 图片验证码类型枚举
*/
@Getter
@RequiredArgsConstructor
public enum CaptchaTypeEnum {
private enum CaptchaImageTypeEnum {
/**
* 算术
......
......@@ -33,4 +33,14 @@ public class CacheConstants {
*/
public static final String LOGIN_USER_CACHE_KEY = "LOGIN_USER";
/**
* 验证码缓存键
*/
public static final String CAPTCHA_CACHE_KEY = "CAPTCHA";
/**
* 限流缓存键
*/
public static final String LIMIT_CACHE_KEY = "LIMIT";
}
......@@ -162,8 +162,8 @@ public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(NotLoginException.class)
public R handleNotLoginException(NotLoginException e, HttpServletRequest request) {
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", request.getRequestURI(), e.getMessage());
return R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源");
log.error("请求地址'{}',认证失败,无法访问系统资源", request.getRequestURI(), e);
return R.fail(HttpStatus.UNAUTHORIZED.value(), "登录状态已过期,请重新登录");
}
/**
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.charles7c.cnadmin.auth.model.vo;
package top.charles7c.cnadmin.common.model.vo;
import java.io.Serializable;
......
/*
* 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.common.util;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import top.charles7c.cnadmin.common.util.validate.CheckUtils;
/**
* 邮件工具类
*
* @author Charles7c
* @since 2023/1/12 23:25
*/
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MailUtils {
private static final JavaMailSender MAIL_SENDER = SpringUtil.getBean(JavaMailSender.class);
/**
* 发送文本邮件给单个人
*
* @param subject
* 主题
* @param content
* 内容
* @param to
* 收件人
* @throws MessagingException
* /
*/
public static void sendText(String to, String subject, String content) throws MessagingException {
send(splitAddress(to), null, null, subject, content, false);
}
/**
* 发送 HTML 邮件给单个人
*
* @param subject
* 主题
* @param content
* 内容
* @param to
* 收件人
* @throws MessagingException
* /
*/
public static void sendHtml(String to, String subject, String content) throws MessagingException {
send(splitAddress(to), null, null, subject, content, true);
}
/**
* 发送 HTML 邮件给单个人
*
* @param subject
* 主题
* @param content
* 内容
* @param to
* 收件人
* @param files
* 附件列表
* @throws MessagingException
* /
*/
public static void sendHtml(String to, String subject, String content, File... files) throws MessagingException {
send(splitAddress(to), null, null, subject, content, true, files);
}
/**
* 发送 HTML 邮件给多个人
*
* @param subject
* 主题
* @param content
* 内容
* @param tos
* 收件人列表
* @param files
* 附件列表
* @throws MessagingException
* /
*/
public static void sendHtml(Collection<String> tos, String subject, String content, File... files)
throws MessagingException {
send(tos, null, null, subject, content, true, files);
}
/**
* 发送 HTML 邮件给多个人
*
* @param subject
* 主题
* @param content
* 内容
* @param tos
* 收件人列表
* @param ccs
* 抄送人列表
* @param files
* 附件列表
* @throws MessagingException
* /
*/
public static void sendHtml(Collection<String> tos, Collection<String> ccs, String subject, String content,
File... files) throws MessagingException {
send(tos, ccs, null, subject, content, true, files);
}
/**
* 发送 HTML 邮件给多个人
*
* @param subject
* 主题
* @param content
* 内容
* @param tos
* 收件人列表
* @param ccs
* 抄送人列表
* @param bccs
* 密送人列表
* @param files
* 附件列表
* @throws MessagingException
* /
*/
public static void sendHtml(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject,
String content, File... files) throws MessagingException {
send(tos, ccs, bccs, subject, content, true, files);
}
/**
* 发送邮件给多个人
*
* @param tos
* 收件人列表
* @param ccs
* 抄送人列表
* @param bccs
* 密送人列表
* @param subject
* 主题
* @param content
* 内容
* @param isHtml
* 是否是 HTML
* @param files
* 附件列表
* @throws MessagingException
* /
*/
public static void send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject,
String content, boolean isHtml, File... files) throws MessagingException {
CheckUtils.exIfCondition(() -> CollUtil.isEmpty(tos), "请至少指定一名收件人");
MimeMessage mimeMessage = MAIL_SENDER.createMimeMessage();
MimeMessageHelper messageHelper =
new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.displayName());
// 设置基本信息
messageHelper.setFrom(SpringUtil.getProperty("spring.mail.username"));
messageHelper.setSubject(subject);
messageHelper.setText(content, isHtml);
// 设置收信人
// 抄送人
if (CollUtil.isNotEmpty(ccs)) {
messageHelper.setCc(ccs.toArray(new String[0]));
}
// 密送人
if (CollUtil.isNotEmpty(bccs)) {
messageHelper.setBcc(bccs.toArray(new String[0]));
}
// 收件人
messageHelper.setTo(tos.toArray(new String[0]));
// 设置附件
if (ArrayUtil.isNotEmpty(files)) {
for (File file : files) {
messageHelper.addAttachment(file.getName(), file);
}
}
// 发送邮件
MAIL_SENDER.send(mimeMessage);
}
/**
* 将多个联系人转为列表,分隔符为逗号或者分号
*
* @param addresses
* 多个联系人,如果为空返回null
* @return 联系人列表
*/
private static List<String> splitAddress(String addresses) {
if (StrUtil.isBlank(addresses)) {
return null;
}
List<String> result;
if (StrUtil.contains(addresses, CharUtil.COMMA)) {
result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
} else if (StrUtil.contains(addresses, ';')) {
result = StrUtil.splitTrim(addresses, ';');
} else {
result = CollUtil.newArrayList(addresses);
}
return result;
}
}
/*
* 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.common.util;
import java.util.Map;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
/**
* 模板工具类
*
* @author Charles7c
* @since 2023/1/13 20:37
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TemplateUtils {
private static final String TEMPLATE_PARENT_PATH = "templates";
/**
* 将模板与绑定参数融合后返回为字符串
*
* @param bindingMap
* 绑定的参数,此Map中的参数会替换模板中的变量
* @return 融合后的内容
*/
public static String render(String templatePath, Map<?, ?> bindingMap) {
TemplateEngine engine =
TemplateUtil.createEngine(new TemplateConfig(TEMPLATE_PARENT_PATH, TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate(templatePath);
return template.render(bindingMap);
}
}
......@@ -38,25 +38,25 @@ public class CheckUtils extends Validator {
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param str
* 被检测的字符串
* @param message
* 错误信息
*/
public static void exIfNull(Object obj, String message) {
exIfNull(obj, message, EXCEPTION_TYPE);
public static void exIfBlank(CharSequence str, String message) {
exIfBlank(str, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
* 如果为空,抛出异常
*
* @param str
* 被检测的字符串
* @param message
* 错误信息
*/
public static void exIfBlank(CharSequence str, String message) {
exIfBlank(str, message, EXCEPTION_TYPE);
public static void exIfNotBlank(CharSequence str, String message) {
exIfNotBlank(str, message, EXCEPTION_TYPE);
}
/**
......@@ -87,6 +87,58 @@ public class CheckUtils extends Validator {
exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
*/
public static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) {
exIfEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
*/
public static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) {
exIfNotEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
*/
public static void exIfNull(Object obj, String message) {
exIfNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
*/
public static void exIfNotNull(Object obj, String message) {
exIfNotNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
......
......@@ -38,25 +38,25 @@ public class ValidationUtils extends Validator {
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param str
* 被检测的字符串
* @param message
* 错误信息
*/
public static void exIfNull(Object obj, String message) {
exIfNull(obj, message, EXCEPTION_TYPE);
public static void exIfBlank(CharSequence str, String message) {
exIfBlank(str, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
* 如果为空,抛出异常
*
* @param str
* 被检测的字符串
* @param message
* 错误信息
*/
public static void exIfBlank(CharSequence str, String message) {
exIfBlank(str, message, EXCEPTION_TYPE);
public static void exIfNotBlank(CharSequence str, String message) {
exIfNotBlank(str, message, EXCEPTION_TYPE);
}
/**
......@@ -87,6 +87,58 @@ public class ValidationUtils extends Validator {
exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
*/
public static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) {
exIfEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
*/
public static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) {
exIfNotEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
*/
public static void exIfNull(Object obj, String message) {
exIfNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
*/
public static void exIfNotNull(Object obj, String message) {
exIfNotNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
......
......@@ -25,6 +25,8 @@ import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
/**
* 校验器
*
* @author Charles7c
* @since 2023/1/2 22:12
*/
......@@ -35,22 +37,19 @@ public class Validator {
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param str
* 被检测的字符串
* @param message
* 错误信息
* @param exceptionType
* 异常类型
*/
protected static void exIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
if (obj == null) {
log.error(message);
throw ReflectUtil.newInstance(exceptionType, message);
}
protected static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> StrUtil.isBlank(str), message, exceptionType);
}
/**
* 如果为空,抛出异常
* 如果为空,抛出异常
*
* @param str
* 被检测的字符串
......@@ -59,11 +58,9 @@ public class Validator {
* @param exceptionType
* 异常类型
*/
public static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) {
if (StrUtil.isBlank(str)) {
log.error(message);
throw ReflectUtil.newInstance(exceptionType, message);
}
protected static void exIfNotBlank(CharSequence str, String message,
Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> StrUtil.isNotBlank(str), message, exceptionType);
}
/**
......@@ -78,12 +75,9 @@ public class Validator {
* @param exceptionType
* 异常类型
*/
public static void exIfEqual(Object obj1, Object obj2, String message,
protected static void exIfEqual(Object obj1, Object obj2, String message,
Class<? extends RuntimeException> exceptionType) {
if (ObjectUtil.equals(obj1, obj2)) {
log.error(message);
throw ReflectUtil.newInstance(exceptionType, message);
}
exIfCondition(() -> ObjectUtil.equal(obj1, obj2), message, exceptionType);
}
/**
......@@ -98,12 +92,71 @@ public class Validator {
* @param exceptionType
* 异常类型
*/
public static void exIfNotEqual(Object obj1, Object obj2, String message,
protected static void exIfNotEqual(Object obj1, Object obj2, String message,
Class<? extends RuntimeException> exceptionType) {
if (ObjectUtil.notEqual(obj1, obj2)) {
log.error(message);
throw ReflectUtil.newInstance(exceptionType, message);
}
exIfCondition(() -> ObjectUtil.notEqual(obj1, obj2), message, exceptionType);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
* @param exceptionType
* 异常类型
*/
protected static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message,
Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> StrUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1
* 要比较的字符串1
* @param str2
* 要比较的字符串2
* @param message
* 错误信息
* @param exceptionType
* 异常类型
*/
protected static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message,
Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> !StrUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
}
/**
* 如果为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
* @param exceptionType
* 异常类型
*/
protected static void exIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> obj == null, message, exceptionType);
}
/**
* 如果不为空,抛出异常
*
* @param obj
* 被检测的对象
* @param message
* 错误信息
* @param exceptionType
* 异常类型
*/
protected static void exIfNotNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
exIfCondition(() -> obj != null, message, exceptionType);
}
/**
......@@ -116,7 +169,7 @@ public class Validator {
* @param exceptionType
* 异常类型
*/
public static void exIfCondition(java.util.function.BooleanSupplier conditionSupplier, String message,
protected static void exIfCondition(java.util.function.BooleanSupplier conditionSupplier, String message,
Class<? extends RuntimeException> exceptionType) {
if (conditionSupplier != null && conditionSupplier.getAsBoolean()) {
log.error(message);
......
......@@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
......@@ -65,20 +66,21 @@ import top.charles7c.cnadmin.monitor.model.entity.SysLog;
public class LogInterceptor implements HandlerInterceptor {
private final LogProperties operationLogProperties;
private static final String ENCRYPT_SYMBOL = "****************";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!checkIsNeedRecord(handler, request)) {
return true;
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) {
if (checkIsNeedRecord(handler, request)) {
// 记录操作时间
this.logCreateTime();
}
// 记录操作时间
this.logCreateTime();
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception e) {
// 记录请求耗时及异常信息
SysLog sysLog = this.logElapsedTimeAndException();
if (sysLog == null) {
......@@ -203,6 +205,7 @@ public class LogInterceptor implements HandlerInterceptor {
* 待脱敏数据
* @return 脱敏后的 JSON 字符串数据
*/
@SuppressWarnings("unchecked")
private String desensitize(Map waitDesensitizeData) {
String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
try {
......@@ -211,9 +214,9 @@ public class LogInterceptor implements HandlerInterceptor {
}
for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************");
waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************");
waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************");
waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
}
return JSONUtil.toJsonStr(waitDesensitizeData);
} catch (Exception ignored) {
......
......@@ -32,12 +32,6 @@ limitations under the License.
<description>系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)</description>
<dependencies>
<!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>
<!-- 公共模块(存放公共工具类,公共配置等) -->
<dependency>
<groupId>top.charles7c</groupId>
......
......@@ -52,7 +52,7 @@ public class LoginServiceImpl implements LoginService {
// 校验
ValidationUtils.exIfNull(sysUser, "用户名或密码错误");
Long userId = sysUser.getUserId();
ValidationUtils.exIfNotEqual(sysUser.getPassword(), SecureUtils.md5Salt(password, userId.toString()),
ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(password, userId.toString()), sysUser.getPassword(),
"用户名或密码错误");
ValidationUtils.exIfEqual(DisEnableStatusEnum.DISABLE, sysUser.getStatus(), "此账号已被禁用,如有疑问,请联系管理员");
......
......@@ -52,6 +52,6 @@ public class UpdateBasicInfoRequest implements Serializable {
* 性别(0未知 1男 2女)
*/
@Schema(description = "性别(0未知 1男 2女)", type = "Integer", allowableValues = {"0", "1", "2"})
@NotNull(message = "非法性别")
@NotNull(message = "性别非法")
private GenderEnum gender;
}
/*
* 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.system.model.request;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hibernate.validator.constraints.Length;
import cn.hutool.core.lang.RegexPool;
/**
* 修改邮箱信息
*
* @author Charles7c
* @since 2023/1/12 20:18
*/
@Data
@Schema(description = "修改邮箱信息")
public class UpdateEmailRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 新邮箱
*/
@Schema(description = "新邮箱")
@NotBlank(message = "新邮箱不能为空")
@Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误")
private String newEmail;
/**
* 验证码
*/
@Schema(description = "验证码")
@NotBlank(message = "验证码不能为空")
@Length(max = 6, message = "验证码非法")
private String captcha;
/**
* 当前密码(加密后)
*/
@Schema(description = "当前密码(加密后)")
@NotBlank(message = "当前密码不能为空")
private String currentPassword;
}
......@@ -67,4 +67,16 @@ public interface UserService {
* 用户 ID
*/
void updatePassword(String oldPassword, String newPassword, Long userId);
/**
* 修改邮箱
*
* @param newEmail
* 新邮箱
* @param currentPassword
* 当前密码
* @param userId
* 用户ID
*/
void updateEmail(String newEmail, String currentPassword, Long userId);
}
......@@ -104,7 +104,7 @@ public class UserServiceImpl implements UserService {
@Transactional(rollbackFor = Exception.class)
public void updatePassword(String oldPassword, String newPassword, Long userId) {
SysUser sysUser = this.getById(userId);
ValidationUtils.exIfNotEqual(sysUser.getPassword(), SecureUtils.md5Salt(oldPassword, userId.toString()),
ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(oldPassword, userId.toString()), sysUser.getPassword(),
"当前密码错误");
// 更新密码和密码重置时间
......@@ -120,6 +120,27 @@ public class UserServiceImpl implements UserService {
LoginHelper.updateLoginUser(loginUser);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateEmail(String newEmail, String currentPassword, Long userId) {
// 校验
SysUser sysUser = this.getById(userId);
ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(currentPassword, userId.toString()), sysUser.getPassword(),
"当前密码错误");
Long count = userMapper.selectCount(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getEmail, newEmail));
ValidationUtils.exIfCondition(() -> count > 0, "邮箱已绑定其他账号,请更换其他邮箱");
ValidationUtils.exIfEqual(newEmail, sysUser.getEmail(), "新邮箱不能与当前邮箱相同");
// 更新邮箱
userMapper.update(null,
new LambdaUpdateWrapper<SysUser>().set(SysUser::getEmail, newEmail).eq(SysUser::getUserId, userId));
// 更新登录用户信息
LoginUser loginUser = LoginHelper.getLoginUser();
loginUser.setEmail(newEmail);
LoginHelper.updateLoginUser(loginUser);
}
/**
* 根据 ID 查询
*
......
{
"name": "continew-admin-ui",
"description": "ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。",
"description": "ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。",
"version": "0.0.1-SNAPSHOT",
"private": true,
"author": "Charles7c",
......
......@@ -2,14 +2,6 @@ import axios from 'axios';
import type { RouteRecordNormalized } from 'vue-router';
import { UserState } from '@/store/modules/login/types';
export interface ImageCaptchaRes {
uuid: string;
img: string;
}
export function getImageCaptcha() {
return axios.get<ImageCaptchaRes>('/captcha/img');
}
export interface LoginReq {
username: string;
password: string;
......
import axios from 'axios';
import qs from 'query-string';
export interface ImageCaptchaRes {
uuid: string;
img: string;
}
export function getImageCaptcha() {
return axios.get<ImageCaptchaRes>('/common/captcha/img');
}
export interface MailCaptchaReq {
email: string;
}
export function getMailCaptcha(params: MailCaptchaReq) {
return axios.get('/common/captcha/mail', {
params,
paramsSerializer: (obj) => {
return qs.stringify(obj);
},
});
}
......@@ -27,4 +27,13 @@ export interface UpdatePasswordReq {
}
export function updatePassword(req: UpdatePasswordReq) {
return axios.patch('/system/user/center/password', req);
}
export interface UpdateEmailReq {
newEmail: string;
captcha: string;
currentPassword: string;
}
export function updateEmail(req: UpdateEmailReq) {
return axios.patch('/system/user/center/email', req);
}
\ No newline at end of file
......@@ -2,6 +2,8 @@
<a-layout-footer class="footer">
{{ `Copyright © 2022-${new Date().getFullYear()} Charles7c` }}
<span>&nbsp;&nbsp;</span>
<a href="https://github.com/Charles7c/continew-admin" target="_blank">{{ $t('title') }}</a>
<span>&nbsp;&nbsp;</span>
<a href="https://beian.miit.gov.cn" target="_blank">津ICP备2022005864号-2</a>
</a-layout-footer>
</template>
......
......@@ -190,7 +190,7 @@
import useLocale from '@/hooks/locale';
import useUser from '@/hooks/user';
import Menu from '@/components/menu/index.vue';
import getAvatar from "@/utils/avatar";
import getAvatar from '@/utils/avatar';
import MessageBox from '../message-box/index.vue';
const appStore = useAppStore();
......
import axios, { Axios, AxiosResponse, AxiosRequestConfig } from "axios";
import axios, { Axios, AxiosResponse, AxiosRequestConfig } from 'axios';
declare module "axios" {
interface AxiosResponse<T = any> {
......
import { useRouter } from 'vue-router';
import { useI18n } from "vue-i18n";
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
import { useLoginStore } from '@/store';
......
import { defineStore } from 'pinia';
import {
getImageCaptcha as getCaptcha,
login as userLogin,
logout as userLogout,
getUserInfo,
LoginReq,
} from '@/api/auth/login';
import { getImageCaptcha as getCaptcha } from '@/api/common/captcha';
import { setToken, clearToken } from '@/utils/auth';
import { removeRouteListener } from '@/utils/route-listener';
import { UserState } from './types';
......
......@@ -29,7 +29,7 @@
<script lang="ts" setup>
import { useLoginStore } from '@/store';
import getAvatar from "@/utils/avatar";
import getAvatar from '@/utils/avatar';
const userInfo = useLoginStore();
</script>
......
......@@ -37,7 +37,7 @@
:placeholder="$t('login.form.placeholder.password')"
size="large"
allow-clear
max-length="50"
max-length="32"
>
<template #prefix>
<icon-lock />
......@@ -82,13 +82,13 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { FieldRule, Message } from "@arco-design/web-vue";
import { FieldRule, Message } from '@arco-design/web-vue';
import { ValidatedError } from '@arco-design/web-vue/es/form/interface';
import { useI18n } from 'vue-i18n';
// import debug from '@/utils/env';
import { encryptByRsa } from "@/utils/encrypt";
import { encryptByRsa } from '@/utils/encrypt';
import { useStorage } from '@vueuse/core';
import { useLoginStore } from '@/store';
import useLoading from '@/hooks/loading';
......
......@@ -53,14 +53,14 @@
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLoginStore } from '@/store';
import { updateBasicInfo } from '@/api/system/user-center';
import useLoading from '@/hooks/loading';
import { FormInstance } from '@arco-design/web-vue/es/form';
import { BasicInfoModel } from '@/api/system/user-center';
import { FieldRule, Message } from "@arco-design/web-vue";
import { FieldRule, Message } from '@arco-design/web-vue';
const { t } = useI18n();
const { loading, setLoading } = useLoading();
......@@ -84,8 +84,8 @@
// 保存
const save = async () => {
const errors = await formRef.value?.validate();
if (loading.value) return;
const errors = await formRef.value?.validate();
if (!errors) {
setLoading(true);
try {
......
......@@ -15,18 +15,206 @@
</a-typography-paragraph>
</div>
<div class="operation">
<a-link>
<a-link @click="toUpdate">
{{ $t('userCenter.securitySettings.button.update') }}
</a-link>
</div>
</template>
</a-list-item-meta>
<a-modal
v-model:visible="visible"
:title="$t('userCenter.securitySettings.updateEmail.modal.title')"
:mask-closable="false"
@cancel="handleCancel"
@before-ok="handleUpdate"
>
<a-form ref="formRef" :model="formData" :rules="rules">
<a-form-item
field="newEmail"
:validate-trigger="['change', 'blur']"
:label="$t('userCenter.securitySettings.updateEmail.form.label.newEmail')"
>
<a-input
v-model="formData.newEmail"
:placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.newEmail')"
size="large"
allow-clear
>
</a-input>
</a-form-item>
<a-form-item
field="captcha"
:validate-trigger="['change', 'blur']"
:label="$t('userCenter.securitySettings.updateEmail.form.label.captcha')"
>
<a-input
v-model="formData.captcha"
:placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.captcha')"
size="large"
style="width: 80%"
allow-clear
max-length="6"
>
</a-input>
<a-button
class="captcha-btn"
type="primary"
size="large"
:loading="captchaLoading"
:disabled="captchaDisable"
@click="sendCaptcha"
>
{{ captchaBtnName }}
</a-button>
</a-form-item>
<a-form-item
field="currentPassword"
:validate-trigger="['change', 'blur']"
:label="$t('userCenter.securitySettings.updateEmail.form.label.currentPassword')"
>
<a-input-password
v-model="formData.currentPassword"
:placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.currentPassword')"
size="large"
allow-clear
max-length="32"
>
</a-input-password>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLoginStore } from '@/store';
import { FormInstance } from '@arco-design/web-vue/es/form';
import useLoading from '@/hooks/loading';
import { FieldRule, Message } from '@arco-design/web-vue';
import { getMailCaptcha } from '@/api/common/captcha';
import { updateEmail } from '@/api/system/user-center';
import { encryptByRsa } from '@/utils/encrypt';
const { t } = useI18n();
const { loading, setLoading } = useLoading();
const loginStore = useLoginStore();
const visible = ref(false);
const captchaBtnNameKey = ref('userCenter.securitySettings.updateEmail.form.sendCaptcha');
const captchaBtnName = computed(() => t(captchaBtnNameKey.value));
const captchaLoading = ref(false);
const captchaDisable = ref(false);
const captchaTime = ref(60);
const captchaTimer = ref();
const formRef = ref<FormInstance>();
const formData = reactive({
newEmail: '',
captcha: '',
currentPassword: '',
});
const rules = computed((): Record<string, FieldRule[]> => {
return {
newEmail: [
{ required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.newEmail') },
{ type: 'email', message: t('userCenter.securitySettings.updateEmail.form.error.match.newEmail') },
{
validator: (value, callback) => {
if (value === loginStore.email) {
callback(t('userCenter.securitySettings.updateEmail.form.error.validator.newEmail'))
} else {
callback()
}
}
}
],
captcha: [
{ required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.captcha') }
],
currentPassword: [
{ required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.currentPassword') }
]
}
});
// 重置验证码相关
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value);
captchaTime.value = 60;
captchaBtnNameKey.value = 'userCenter.securitySettings.updateEmail.form.sendCaptcha';
captchaDisable.value = false;
}
// 发送验证码
const sendCaptcha = async () => {
if (captchaLoading.value) return;
const errors = await formRef.value?.validateField('newEmail');
if (errors) return;
captchaLoading.value = true;
captchaBtnNameKey.value = 'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha';
try {
const res = await getMailCaptcha({
email: formData.newEmail
});
if (res.success) {
captchaLoading.value = false;
captchaDisable.value = true;
captchaBtnNameKey.value = `${t('userCenter.securitySettings.updateEmail.form.reSendCaptcha')}(${captchaTime.value -= 1}s)`;
Message.success(res.msg);
captchaTimer.value = window.setInterval(function() {
captchaTime.value -= 1;
captchaBtnNameKey.value = `${t('userCenter.securitySettings.updateEmail.form.reSendCaptcha')}(${captchaTime.value}s)`;
if (captchaTime.value < 0) {
window.clearInterval(captchaTimer.value);
captchaTime.value = 60;
captchaBtnNameKey.value = t('userCenter.securitySettings.updateEmail.form.reSendCaptcha');
captchaDisable.value = false;
}
}, 1000)
}
} catch (err) {
resetCaptcha();
captchaLoading.value = false;
console.log((err as Error));
}
};
// 确定修改
const handleUpdate = async () => {
if (loading.value) return false;
const errors = await formRef.value?.validate();
if (errors) return false;
setLoading(true);
try {
const res = await updateEmail({
newEmail: formData.newEmail,
captcha: formData.captcha,
currentPassword: encryptByRsa(formData.currentPassword) || '',
});
await loginStore.getInfo();
if (res.success) Message.success(res.msg);
} finally {
setLoading(false);
}
return true;
};
// 取消修改
const handleCancel = () => {
visible.value = false;
formRef.value?.resetFields();
resetCaptcha();
};
// 打开修改窗口
const toUpdate = () => {
visible.value = true;
};
</script>
<style scoped lang="less"></style>
<style scoped lang="less">
.captcha-btn {
margin-left: 5px;
}
</style>
......@@ -22,12 +22,14 @@
</template>
</a-list-item-meta>
<a-modal v-model:visible="visible" :title="$t('userCenter.securitySettings.updatePwd.modal.title')" @cancel="handleCancel" @before-ok="handleUpdate">
<a-form
ref="formRef"
:model="formData"
:rules="rules"
>
<a-modal
v-model:visible="visible"
:title="$t('userCenter.securitySettings.updatePwd.modal.title')"
:mask-closable="false"
@cancel="handleCancel"
@before-ok="handleUpdate"
>
<a-form ref="formRef" :model="formData" :rules="rules">
<a-form-item
field="oldPassword"
:validate-trigger="['change', 'blur']"
......@@ -38,7 +40,7 @@
:placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.oldPassword')"
size="large"
allow-clear
max-length="50"
max-length="32"
>
</a-input-password>
</a-form-item>
......@@ -52,7 +54,7 @@
:placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.newPassword')"
size="large"
allow-clear
max-length="50"
max-length="32"
>
</a-input-password>
</a-form-item>
......@@ -66,7 +68,7 @@
:placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.rePassword')"
size="large"
allow-clear
max-length="50"
max-length="32"
>
</a-input-password>
</a-form-item>
......@@ -75,14 +77,14 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from "vue";
import { useI18n } from "vue-i18n";
import { ref, reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLoginStore } from '@/store';
import { FormInstance } from "@arco-design/web-vue/es/form";
import useLoading from "@/hooks/loading";
import { FieldRule, Message } from "@arco-design/web-vue";
import { updatePassword } from "@/api/system/user-center";
import { encryptByRsa } from "@/utils/encrypt";
import { FormInstance } from '@arco-design/web-vue/es/form';
import useLoading from '@/hooks/loading';
import { FieldRule, Message } from '@arco-design/web-vue';
import { updatePassword } from '@/api/system/user-center';
import { encryptByRsa } from '@/utils/encrypt';
const { t } = useI18n();
const { loading, setLoading } = useLoading();
......@@ -129,8 +131,8 @@
// 确定修改
const handleUpdate = async () => {
const errors = await formRef.value?.validate();
if (loading.value) return false;
const errors = await formRef.value?.validate();
if (errors) return false;
setLoading(true);
try {
......
......@@ -61,8 +61,8 @@
} from '@arco-design/web-vue/es/upload/interfaces';
import { useLoginStore } from '@/store';
import { uploadAvatar } from '@/api/system/user-center';
import getAvatar from "@/utils/avatar";
import { Message } from "@arco-design/web-vue";
import getAvatar from '@/utils/avatar';
import { Message } from '@arco-design/web-vue';
const loginStore = useLoginStore();
const avatar = {
......
......@@ -63,5 +63,23 @@ export default {
'userCenter.securitySettings.updateEmail.placeholder.error.email':
'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.',
'userCenter.securitySettings.updateEmail.modal.title': 'Update email',
'userCenter.securitySettings.updateEmail.form.label.newEmail': 'New email',
'userCenter.securitySettings.updateEmail.form.label.captcha': 'Captcha',
'userCenter.securitySettings.updateEmail.form.label.currentPassword': 'Current password',
'userCenter.securitySettings.updateEmail.form.sendCaptcha': 'Send captcha',
'userCenter.securitySettings.updateEmail.form.reSendCaptcha': 'Resend captcha',
'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha': 'Sending...',
'userCenter.securitySettings.updateEmail.form.placeholder.newEmail': 'Please enter new email',
'userCenter.securitySettings.updateEmail.form.placeholder.captcha': 'Please enter email captcha',
'userCenter.securitySettings.updateEmail.form.placeholder.currentPassword': 'Please enter current password',
'userCenter.securitySettings.updateEmail.form.error.required.newEmail': 'Please enter new email',
'userCenter.securitySettings.updateEmail.form.error.match.newEmail': 'Please enter the correct email',
'userCenter.securitySettings.updateEmail.form.error.validator.newEmail': 'New email cannot be the same as the old email',
'userCenter.securitySettings.updateEmail.form.error.required.captcha': 'Please enter email captcha',
'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': 'Please enter current password',
'userCenter.securitySettings.button.update': 'Update',
};
......@@ -63,5 +63,23 @@ export default {
'userCenter.securitySettings.updateEmail.placeholder.error.email':
'您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。',
'userCenter.securitySettings.updateEmail.modal.title': '修改邮箱',
'userCenter.securitySettings.updateEmail.form.label.newEmail': '新邮箱',
'userCenter.securitySettings.updateEmail.form.label.captcha': '验证码',
'userCenter.securitySettings.updateEmail.form.label.currentPassword': '当前密码',
'userCenter.securitySettings.updateEmail.form.sendCaptcha': '发送验证码',
'userCenter.securitySettings.updateEmail.form.reSendCaptcha': '重新发送',
'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha': '发送中...',
'userCenter.securitySettings.updateEmail.form.placeholder.newEmail': '请输入新邮箱',
'userCenter.securitySettings.updateEmail.form.placeholder.captcha': '请输入邮箱验证码',
'userCenter.securitySettings.updateEmail.form.placeholder.currentPassword': '请输入当前密码',
'userCenter.securitySettings.updateEmail.form.error.required.newEmail': '请输入新邮箱',
'userCenter.securitySettings.updateEmail.form.error.match.newEmail': '请输入正确的邮箱',
'userCenter.securitySettings.updateEmail.form.error.validator.newEmail': '新邮箱不能与当前邮箱相同',
'userCenter.securitySettings.updateEmail.form.error.required.captcha': '请输入邮箱验证码',
'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': '请输入当前密码',
'userCenter.securitySettings.button.update': '修改',
};
......@@ -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();
}
......
......@@ -14,16 +14,21 @@
* limitations under the License.
*/
package top.charles7c.cnadmin.webapi.controller.auth;
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;
......@@ -31,12 +36,18 @@ 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.auth.config.properties.CaptchaProperties;
import top.charles7c.cnadmin.auth.model.vo.CaptchaVO;
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.RedisUtils;
import top.charles7c.cnadmin.common.util.*;
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
/**
* 验证码 API
......@@ -46,27 +57,60 @@ import top.charles7c.cnadmin.common.util.RedisUtils;
*/
@Tag(name = "验证码 API")
@SaIgnore
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/captcha", produces = MediaType.APPLICATION_JSON_VALUE)
@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() {
// 生成验证码
Captcha captcha = captchaProperties.getCaptcha();
CaptchaProperties.CaptchaImage captchaImage = captchaProperties.getImage();
Captcha captcha = captchaImage.getCaptcha();
// 保存验证码
String uuid = IdUtil.fastSimpleUUID();
String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), uuid);
String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, uuid);
RedisUtils.setCacheObject(captchaKey, captcha.text(),
Duration.ofMinutes(captchaProperties.getExpirationInMinutes()));
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));
}
}
......@@ -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("修改成功");
}
}
......@@ -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:
......
......@@ -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:
......
--- ### 项目配置
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:
......
<!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>
\ No newline at end of file
......@@ -25,7 +25,7 @@ limitations under the License.
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。</description>
<description>ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。</description>
<url>https://github.com/Charles7c/continew-admin</url>
<modules>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册