diff --git a/README.md b/README.md index 35722cb2..220a9973 100644 --- a/README.md +++ b/README.md @@ -218,29 +218,30 @@ pnpm dev ## 核心技术栈 -| 名称 | 版本 | 简介 | -| :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- | -| Vue | 3.3.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | -| TypeScript | 4.9.5 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | -| Arco Design Vue | 2.52.0 | 字节跳动推出的前端 UI 框架,样式美观,组件丰富。 | -| Spring Boot | 2.7.16 | 简化新 Spring 应用的初始搭建以及开发过程。 | -| Undertow | 2.2.26.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | -| Sa-Token + JWT | 1.36.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 | -| MariaDB | 10.10.2 | MySQL 的一个分支,主要由开源社区在维护,完全兼容 MySQL,包括 API 和命令行,能轻松成为 MySQL 的代替品。 | -| MyBatis Plus | 3.5.3.2 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 | +| 名称 | 版本 | 简介 | +|:----------------------------------------------------------------------------------------------------------------------------------|:-------------| :----------------------------------------------------------- | +| Vue | 3.3.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | +| TypeScript | 4.9.5 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | +| Arco Design Vue | 2.52.0 | 字节跳动推出的前端 UI 框架,样式美观,组件丰富。 | +| Spring Boot | 2.7.16 | 简化新 Spring 应用的初始搭建以及开发过程。 | +| Undertow | 2.2.26.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | +| Sa-Token + JWT | 1.36.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 | +| MariaDB | 10.10.2 | MySQL 的一个分支,主要由开源社区在维护,完全兼容 MySQL,包括 API 和命令行,能轻松成为 MySQL 的代替品。 | +| MyBatis Plus | 3.5.3.2 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 | | dynamic-datasource-spring-boot-starter | 3.6.1 | 基于 Spring Boot 的快速集成多数据源的启动器。 | -| Hikari | 4.0.3 | JDBC 连接池,号称 “史上最快连接池”,SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 | -| mysql-connector-j | 8.0.33 | MySQL Java 驱动。 | -| P6Spy | 3.9.1 | SQL 性能分析组件。 | -| Liquibase | 4.9.1 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 | -| Redis | 6.2.7 | 高性能的 key-value 数据库。 | -| Redisson | 3.20.1 | 不仅仅是一个 Redis Java 客户端,同其他 Redis Java 客户端有着很大的区别,相比之下其他客户端提供的功能还仅仅停留在作为数据库驱动层面上,比如仅针对 Redis 提供连接方式,发送命令和处理返回结果等。而 Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 | -| Just Auth | 1.16.5 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! | -| Easy Excel | 3.3.2 | 一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 | -| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目。 | -| Knife4j | 4.3.0 | 前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案。本项目使用的是 knife4j-openapi3-spring-boot-starter 基于 OpenAPI3 规范,在 Spring Boot < 3.0.0-M1 的单体架构下可以直接引用此 starter,该模块包含了 UI 部分,底层基于 springdoc-openapi 项目。 | -| Hutool | 5.8.22 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 | -| Lombok | 1.18.30 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 | +| Hikari | 4.0.3 | JDBC 连接池,号称 “史上最快连接池”,SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 | +| mysql-connector-j | 8.0.33 | MySQL Java 驱动。 | +| P6Spy | 3.9.1 | SQL 性能分析组件。 | +| Liquibase | 4.9.1 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 | +| Redis | 6.2.7 | 高性能的 key-value 数据库。 | +| Redisson | 3.20.1 | 不仅仅是一个 Redis Java 客户端,同其他 Redis Java 客户端有着很大的区别,相比之下其他客户端提供的功能还仅仅停留在作为数据库驱动层面上,比如仅针对 Redis 提供连接方式,发送命令和处理返回结果等。而 Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 | +| SMS4J | 3.0.3 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 | +| Just Auth | 1.16.5 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! | +| Easy Excel | 3.3.2 | 一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 | +| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目。 | +| Knife4j | 4.3.0 | 前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案。本项目使用的是 knife4j-openapi3-spring-boot-starter 基于 OpenAPI3 规范,在 Spring Boot < 3.0.0-M1 的单体架构下可以直接引用此 starter,该模块包含了 UI 部分,底层基于 springdoc-openapi 项目。 | +| Hutool | 5.8.22 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 | +| Lombok | 1.18.30 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 | ## 项目结构 diff --git a/continew-admin-common/pom.xml b/continew-admin-common/pom.xml index 7ddbde24..4d62bb89 100644 --- a/continew-admin-common/pom.xml +++ b/continew-admin-common/pom.xml @@ -115,6 +115,12 @@ limitations under the License. + + + org.dromara.sms4j + sms4j-spring-boot-starter + + com.xkcoding.justauth diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/CaptchaProperties.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/CaptchaProperties.java index e9e40ae3..7f4b6c9f 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/CaptchaProperties.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/CaptchaProperties.java @@ -52,6 +52,11 @@ public class CaptchaProperties { */ private CaptchaMail mail; + /** + * 短信验证码配置 + */ + private CaptchaSms sms; + /** * 图片验证码配置 */ @@ -133,6 +138,32 @@ public class CaptchaProperties { private String templatePath; } + /** + * 短信验证码配置 + */ + @Data + public static class CaptchaSms { + /** + * 内容长度 + */ + private int length; + + /** + * 过期时间 + */ + private long expirationInMinutes; + + /** + * 限制时间 + */ + private long limitInSeconds; + + /** + * 模板 ID + */ + private String templateId; + } + /** * 图片验证码类型枚举 */ diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/request/PhoneLoginRequest.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/request/PhoneLoginRequest.java new file mode 100644 index 00000000..8beea73f --- /dev/null +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/request/PhoneLoginRequest.java @@ -0,0 +1,59 @@ +/* + * 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.auth.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 top.charles7c.cnadmin.common.constant.RegexConsts; + +/** + * 手机号登录信息 + * + * @author Charles7c + * @since 2023/10/26 22:37 + */ +@Data +@Schema(description = "手机号登录信息") +public class PhoneLoginRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 手机号 + */ + @Schema(description = "手机号", example = "13811111111") + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = RegexConsts.MOBILE, message = "手机号格式错误") + private String phone; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "8888") + @NotBlank(message = "验证码不能为空") + @Length(max = 4, message = "验证码非法") + private String captcha; +} diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/LoginService.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/LoginService.java index a7738af5..cc4c5e9d 100644 --- a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/LoginService.java +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/LoginService.java @@ -41,6 +41,15 @@ public interface LoginService { */ String accountLogin(String username, String password); + /** + * 手机号登录 + * + * @param phone + * 手机号 + * @return 令牌 + */ + String phoneLogin(String phone); + /** * 邮箱登录 * diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/impl/LoginServiceImpl.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/impl/LoginServiceImpl.java index d5e098b8..22fcf8a4 100644 --- a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/impl/LoginServiceImpl.java +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/service/impl/LoginServiceImpl.java @@ -85,6 +85,14 @@ public class LoginServiceImpl implements LoginService { return this.login(user); } + @Override + public String phoneLogin(String phone) { + UserDO user = userService.getByPhone(phone); + CheckUtils.throwIfNull(user, "此手机号未绑定本系统账号"); + this.checkUserStatus(user); + return this.login(user); + } + @Override public String emailLogin(String email) { UserDO user = userService.getByEmail(email); diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/mapper/UserMapper.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/mapper/UserMapper.java index e2e7751e..ea6e034c 100644 --- a/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/mapper/UserMapper.java +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/mapper/UserMapper.java @@ -40,6 +40,16 @@ public interface UserMapper extends DataPermissionMapper { @Select("SELECT * FROM `sys_user` WHERE `username` = #{username}") UserDO selectByUsername(@Param("username") String username); + /** + * 根据手机号查询 + * + * @param phone + * 手机号 + * @return 用户信息 + */ + @Select("SELECT * FROM `sys_user` WHERE `phone` = #{phone}") + UserDO selectByPhone(@Param("phone") String phone); + /** * 根据邮箱查询 * diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/model/request/UserPhoneUpdateRequest.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/model/request/UserPhoneUpdateRequest.java new file mode 100644 index 00000000..49493a8c --- /dev/null +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/model/request/UserPhoneUpdateRequest.java @@ -0,0 +1,67 @@ +/* + * 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 top.charles7c.cnadmin.common.constant.RegexConsts; + +/** + * 用户手机号修改信息 + * + * @author Charles7c + * @since 2023/10/27 20:11 + */ +@Data +@Schema(description = "用户手机号修改信息") +public class UserPhoneUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 新手机号 + */ + @Schema(description = "新手机号", example = "13811111111") + @NotBlank(message = "新手机号不能为空") + @Pattern(regexp = RegexConsts.MOBILE, message = "手机号格式错误") + private String newPhone; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "8888") + @NotBlank(message = "验证码不能为空") + @Length(max = 4, message = "验证码非法") + private String captcha; + + /** + * 当前密码(加密后) + */ + @Schema(description = "当前密码(加密后)", + example = "SYRLSszQGcMv4kP2Yolou9zf28B9GDakR9u91khxmR7V++i5A384kwnNZxqgvT6bjT4zqpIDuMFLWSt92hQJJA==") + @NotBlank(message = "当前密码不能为空") + private String currentPassword; +} diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/service/UserService.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/service/UserService.java index 93fc01fd..9417fa38 100644 --- a/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/service/UserService.java +++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/system/service/UserService.java @@ -79,6 +79,18 @@ public interface UserService extends BaseService 0, "手机号已绑定其他账号,请更换其他手机号"); + CheckUtils.throwIfEqual(newPhone, user.getPhone(), "新手机号不能与当前手机号相同"); + // 更新手机号 + baseMapper.lambdaUpdate().set(UserDO::getPhone, newPhone).eq(UserDO::getId, id).update(); + } + @Override @Transactional(rollbackFor = Exception.class) public void updateEmail(String newEmail, String currentPassword, Long id) { @@ -244,6 +255,11 @@ public class UserServiceImpl extends BaseServiceImpl(`${BASE_URL}/account`, req); } @@ -29,6 +28,15 @@ export function emailLogin(req: EmailLoginReq) { return axios.post(`${BASE_URL}/email`, req); } +export interface PhoneLoginReq { + phone: string; + captcha: string; +} + +export function phoneLogin(req: PhoneLoginReq) { + return axios.post(`${BASE_URL}/phone`, req); +} + export function logout() { return axios.post(`${BASE_URL}/logout`); } diff --git a/continew-admin-ui/src/api/common/captcha.ts b/continew-admin-ui/src/api/common/captcha.ts index 5ae9c560..d8efb0f5 100644 --- a/continew-admin-ui/src/api/common/captcha.ts +++ b/continew-admin-ui/src/api/common/captcha.ts @@ -1,22 +1,19 @@ import axios from 'axios'; -import qs from 'query-string'; + +const BASE_URL = '/common/captcha'; export interface ImageCaptchaRes { uuid: string; img: string; } export function getImageCaptcha() { - return axios.get('/common/captcha/img'); + return axios.get(`${BASE_URL}/img`); } -export interface MailCaptchaReq { - email: string; +export function getMailCaptcha(email: string) { + return axios.get(`${BASE_URL}/mail?email=${email}`); } -export function getMailCaptcha(params: MailCaptchaReq) { - return axios.get('/common/captcha/mail', { - params, - paramsSerializer: (obj) => { - return qs.stringify(obj); - }, - }); + +export function getSmsCaptcha(phone: string) { + return axios.get(`${BASE_URL}/sms?phone=${phone}`); } diff --git a/continew-admin-ui/src/api/system/user-center.ts b/continew-admin-ui/src/api/system/user-center.ts index e96e8398..5b63a239 100644 --- a/continew-admin-ui/src/api/system/user-center.ts +++ b/continew-admin-ui/src/api/system/user-center.ts @@ -16,31 +16,41 @@ export function uploadAvatar(data: FormData) { return axios.post(`${BASE_URL}/avatar`, data); } -export interface UpdateBasicInfoReq { +export interface UserBasicInfoUpdateReq { nickname: string; gender: number; } -export function updateBasicInfo(req: UpdateBasicInfoReq) { +export function updateBasicInfo(req: UserBasicInfoUpdateReq) { return axios.patch(`${BASE_URL}/basic/info`, req); } -export interface UpdatePasswordReq { +export interface UserPasswordUpdateReq { oldPassword: string; newPassword: string; } -export function updatePassword(req: UpdatePasswordReq) { +export function updatePassword(req: UserPasswordUpdateReq) { return axios.patch(`${BASE_URL}/password`, req); } -export interface UpdateEmailReq { +export interface UserPhoneUpdateReq { + newPhone: string; + captcha: string; + currentPassword: string; +} + +export function updatePhone(req: UserPhoneUpdateReq) { + return axios.patch(`${BASE_URL}/phone`, req); +} + +export interface UserEmailUpdateReq { newEmail: string; captcha: string; currentPassword: string; } -export function updateEmail(req: UpdateEmailReq) { +export function updateEmail(req: UserEmailUpdateReq) { return axios.patch(`${BASE_URL}/email`, req); } diff --git a/continew-admin-ui/src/store/modules/user/index.ts b/continew-admin-ui/src/store/modules/user/index.ts index 16007ef5..4f4e247a 100644 --- a/continew-admin-ui/src/store/modules/user/index.ts +++ b/continew-admin-ui/src/store/modules/user/index.ts @@ -1,9 +1,11 @@ import { defineStore } from 'pinia'; import { - LoginReq, + AccountLoginReq, EmailLoginReq, + PhoneLoginReq, accountLogin as userAccountLogin, emailLogin as userEmailLogin, + phoneLogin as userPhoneLogin, socialLogin as userSocialLogin, logout as userLogout, getUserInfo, @@ -45,7 +47,7 @@ const useUserStore = defineStore('user', { }, // 账号登录 - async accountLogin(req: LoginReq) { + async accountLogin(req: AccountLoginReq) { try { const res = await userAccountLogin(req); setToken(res.data.token); @@ -66,6 +68,17 @@ const useUserStore = defineStore('user', { } }, + // 手机号登录 + async phoneLogin(req: PhoneLoginReq) { + try { + const res = await userPhoneLogin(req); + setToken(res.data.token); + } catch (err) { + clearToken(); + throw err; + } + }, + // 三方账号身份登录 async socialLogin(source: string, req: any) { try { diff --git a/continew-admin-ui/src/views/login/components/account-login.vue b/continew-admin-ui/src/views/login/components/account-login.vue index 4e448be0..b7793831 100644 --- a/continew-admin-ui/src/views/login/components/account-login.vue +++ b/continew-admin-ui/src/views/login/components/account-login.vue @@ -57,7 +57,7 @@ import { useI18n } from 'vue-i18n'; import { useStorage } from '@vueuse/core'; import { useUserStore } from '@/store'; - import { LoginReq } from '@/api/auth'; + import { AccountLoginReq } from '@/api/auth'; import { ValidatedError } from '@arco-design/web-vue'; import { encryptByRsa } from '@/utils/encrypt'; import { useRouter } from 'vue-router'; @@ -81,7 +81,7 @@ password: loginConfig.value.password, captcha: '', uuid: '', - } as LoginReq, + } as AccountLoginReq, rules: { username: [ { required: true, message: t('login.account.error.required.username') }, diff --git a/continew-admin-ui/src/views/login/components/email-login.vue b/continew-admin-ui/src/views/login/components/email-login.vue index 1b2896b5..a4c8e88d 100644 --- a/continew-admin-ui/src/views/login/components/email-login.vue +++ b/continew-admin-ui/src/views/login/components/email-login.vue @@ -93,9 +93,7 @@ if (!valid) { captchaLoading.value = true; captchaBtnNameKey.value = 'login.captcha.ing'; - getMailCaptcha({ - email: form.value.email, - }) + getMailCaptcha(form.value.email) .then((res) => { captchaLoading.value = false; captchaDisable.value = true; @@ -108,10 +106,7 @@ captchaTime.value }s)`; if (captchaTime.value <= 0) { - window.clearInterval(captchaTimer.value); - captchaTime.value = 60; - captchaBtnNameKey.value = t('login.captcha.get'); - captchaDisable.value = false; + resetCaptcha(); } }, 1000); proxy.$message.success(res.msg); diff --git a/continew-admin-ui/src/views/login/components/phone-login.vue b/continew-admin-ui/src/views/login/components/phone-login.vue index d7e99ea7..be51083f 100644 --- a/continew-admin-ui/src/views/login/components/phone-login.vue +++ b/continew-admin-ui/src/views/login/components/phone-login.vue @@ -6,6 +6,7 @@ layout="vertical" size="large" class="login-form" + @submit="handleLogin" > @@ -20,7 +21,7 @@ @@ -33,8 +34,13 @@ {{ captchaBtnName }} - {{ $t('login.button') }}(即将开放) + {{ $t('login.button') }}(演示不开放) @@ -42,21 +48,28 @@ + diff --git a/continew-admin-ui/src/views/system/user/center/locale/en-US.ts b/continew-admin-ui/src/views/system/user/center/locale/en-US.ts index 40c72279..bf766c0d 100644 --- a/continew-admin-ui/src/views/system/user/center/locale/en-US.ts +++ b/continew-admin-ui/src/views/system/user/center/locale/en-US.ts @@ -74,6 +74,24 @@ export default { 'It is used to receive messages, verify identity, and support mobile phone verification code login after binding', 'userCenter.securitySettings.phone.content': 'Unbound', + 'userCenter.securitySettings.updatePhone.modal.title': 'Update phone', + 'userCenter.securitySettings.updatePhone.form.label.newPhone': 'New phone', + 'userCenter.securitySettings.updatePhone.form.label.captcha': 'Captcha', + 'userCenter.securitySettings.updatePhone.form.label.currentPassword': + 'Current password', + + 'userCenter.securitySettings.updatePhone.form.placeholder.newPhone': + 'Please enter new phone', + 'userCenter.securitySettings.updatePhone.form.placeholder.currentPassword': + 'Please enter current password', + + 'userCenter.securitySettings.updatePhone.form.error.required.newPhone': + 'Please enter new phone', + 'userCenter.securitySettings.updatePhone.form.error.match.newPhone': + 'Please enter the correct phone', + 'userCenter.securitySettings.updatePhone.form.error.required.currentPassword': + 'Please enter current password', + // update-email 'userCenter.securitySettings.email.label': 'Email', 'userCenter.securitySettings.email.tip': @@ -85,16 +103,9 @@ export default { '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', @@ -102,8 +113,6 @@ export default { 'Please enter new email', 'userCenter.securitySettings.updateEmail.form.error.match.newEmail': 'Please enter the correct email', - 'userCenter.securitySettings.updateEmail.form.error.required.captcha': - 'Please enter email captcha', 'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': 'Please enter current password', @@ -115,4 +124,10 @@ export default { 'userCenter.securitySettings.content.hasBeenSet': 'Has been set', 'userCenter.securitySettings.button.update': 'Update', + 'userCenter.securitySettings.captcha.get': 'Get captcha', + 'userCenter.securitySettings.captcha.ing': 'Sending...', + 'userCenter.securitySettings.form.placeholder.captcha': + 'Please enter captcha', + 'userCenter.securitySettings.form.error.required.captcha': + 'Please enter captcha', }; diff --git a/continew-admin-ui/src/views/system/user/center/locale/zh-CN.ts b/continew-admin-ui/src/views/system/user/center/locale/zh-CN.ts index 1132c792..f367805c 100644 --- a/continew-admin-ui/src/views/system/user/center/locale/zh-CN.ts +++ b/continew-admin-ui/src/views/system/user/center/locale/zh-CN.ts @@ -70,6 +70,25 @@ export default { '用于接收消息、验证身份,绑定后可支持手机验证码登录', 'userCenter.securitySettings.phone.content': '未绑定', + 'userCenter.securitySettings.updatePhone.modal.title': '修改手机号', + 'userCenter.securitySettings.updatePhone.form.label.newPhone': '新手机号', + 'userCenter.securitySettings.updatePhone.form.label.captcha': '验证码', + 'userCenter.securitySettings.updatePhone.form.label.currentPassword': + '当前密码', + + 'userCenter.securitySettings.updatePhone.form.placeholder.newPhone': + '请输入新手机号', + 'userCenter.securitySettings.updatePhone.form.placeholder.currentPassword': + '请输入当前密码', + + 'userCenter.securitySettings.updatePhone.form.error.required.newPhone': + '请输入新手机号', + 'userCenter.securitySettings.updatePhone.form.error.match.newPhone': + '请输入正确的手机号', + + 'userCenter.securitySettings.updatePhone.form.error.required.currentPassword': + '请输入当前密码', + // update-email 'userCenter.securitySettings.email.label': '安全邮箱', 'userCenter.securitySettings.email.tip': @@ -81,15 +100,9 @@ export default { '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': '请输入当前密码', @@ -97,8 +110,6 @@ export default { '请输入新邮箱', 'userCenter.securitySettings.updateEmail.form.error.match.newEmail': '请输入正确的邮箱', - 'userCenter.securitySettings.updateEmail.form.error.required.captcha': - '请输入邮箱验证码', 'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': '请输入当前密码', @@ -109,4 +120,8 @@ export default { 'userCenter.securitySettings.content.hasBeenSet': '已设置', 'userCenter.securitySettings.button.update': '修改', + 'userCenter.securitySettings.captcha.get': '获取验证码', + 'userCenter.securitySettings.captcha.ing': '发送中...', + 'userCenter.securitySettings.form.placeholder.captcha': '请输入验证码', + 'userCenter.securitySettings.form.error.required.captcha': '请输入验证码', }; diff --git a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/AuthController.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/AuthController.java index 03c735a1..2bcc4344 100644 --- a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/AuthController.java +++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/AuthController.java @@ -34,6 +34,7 @@ import cn.hutool.core.bean.BeanUtil; import top.charles7c.cnadmin.auth.model.request.AccountLoginRequest; import top.charles7c.cnadmin.auth.model.request.EmailLoginRequest; +import top.charles7c.cnadmin.auth.model.request.PhoneLoginRequest; import top.charles7c.cnadmin.auth.model.vo.LoginVO; import top.charles7c.cnadmin.auth.model.vo.RouteVO; import top.charles7c.cnadmin.auth.model.vo.UserInfoVO; @@ -96,6 +97,20 @@ public class AuthController { return LoginVO.builder().token(token).build(); } + @SaIgnore + @Operation(summary = "手机号登录", description = "根据手机号和验证码进行登录认证") + @PostMapping("/phone") + public LoginVO phoneLogin(@Validated @RequestBody PhoneLoginRequest loginRequest) { + String phone = loginRequest.getPhone(); + String captchaKey = RedisUtils.formatKey(CacheConsts.CAPTCHA_KEY_PREFIX, phone); + String captcha = RedisUtils.getCacheObject(captchaKey); + ValidationUtils.throwIfBlank(captcha, "验证码已失效"); + ValidationUtils.throwIfNotEqualIgnoreCase(loginRequest.getCaptcha(), captcha, "验证码错误"); + RedisUtils.deleteCacheObject(captchaKey); + String token = loginService.phoneLogin(phone); + return LoginVO.builder().token(token).build(); + } + @SaIgnore @Operation(summary = "用户退出", description = "注销用户的当前登录") @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", diff --git a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/common/CaptchaController.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/common/CaptchaController.java index 6909a1de..afae54ce 100644 --- a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/common/CaptchaController.java +++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/common/CaptchaController.java @@ -17,6 +17,8 @@ package top.charles7c.cnadmin.webapi.controller.common; import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; import javax.mail.MessagingException; import javax.validation.constraints.NotBlank; @@ -27,6 +29,10 @@ import lombok.RequiredArgsConstructor; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.dromara.sms4j.api.SmsBlend; +import org.dromara.sms4j.api.entity.SmsResponse; +import org.dromara.sms4j.comm.constant.SupplierConstant; +import org.dromara.sms4j.core.factory.SmsFactory; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -36,6 +42,7 @@ import com.wf.captcha.base.Captcha; import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.lang.Dict; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; @@ -90,22 +97,47 @@ public class CaptchaController { String captchaKeyPrefix = CacheConsts.CAPTCHA_KEY_PREFIX; String limitCaptchaKey = RedisUtils.formatKey(limitKeyPrefix, captchaKeyPrefix, email); long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey); - CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送邮箱验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000); - + CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送验证码过于频繁,请您 {}s 后再试", 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】邮箱验证码", projectProperties.getName()), content); - // 保存验证码 String captchaKey = RedisUtils.formatKey(captchaKeyPrefix, email); RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes)); RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaMail.getLimitInSeconds())); return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes)); } + + @Operation(summary = "获取短信验证码", description = "发送验证码到指定手机号") + @GetMapping("/sms") + public R getSmsCaptcha( + @NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexConsts.MOBILE, message = "手机号格式错误") String phone) { + String limitKeyPrefix = CacheConsts.LIMIT_KEY_PREFIX; + String captchaKeyPrefix = CacheConsts.CAPTCHA_KEY_PREFIX; + String limitCaptchaKey = RedisUtils.formatKey(limitKeyPrefix, captchaKeyPrefix, phone); + long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey); + CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000); + // 生成验证码 + CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms(); + String captcha = RandomUtil.randomNumbers(captchaSms.getLength()); + // 发送验证码 + Long expirationInMinutes = captchaSms.getExpirationInMinutes(); + SmsBlend smsBlend = SmsFactory.getBySupplier(SupplierConstant.CLOOPEN); + Map messageMap = MapUtil.newHashMap(2, true); + messageMap.put("captcha", captcha); + messageMap.put("expirationInMinutes", String.valueOf(expirationInMinutes)); + SmsResponse smsResponse = + smsBlend.sendMessage(phone, captchaSms.getTemplateId(), (LinkedHashMap)messageMap); + CheckUtils.throwIf(!smsResponse.isSuccess(), "验证码发送失败"); + // 保存验证码 + String captchaKey = RedisUtils.formatKey(captchaKeyPrefix, phone); + RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes)); + RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaSms.getLimitInSeconds())); + return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes)); + } } diff --git a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/system/UserCenterController.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/system/UserCenterController.java index fafc3c78..2ae06559 100644 --- a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/system/UserCenterController.java +++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/system/UserCenterController.java @@ -49,6 +49,7 @@ import top.charles7c.cnadmin.system.model.entity.UserSocialDO; import top.charles7c.cnadmin.system.model.request.UserBasicInfoUpdateRequest; import top.charles7c.cnadmin.system.model.request.UserEmailUpdateRequest; import top.charles7c.cnadmin.system.model.request.UserPasswordUpdateRequest; +import top.charles7c.cnadmin.system.model.request.UserPhoneUpdateRequest; import top.charles7c.cnadmin.system.model.vo.AvatarVO; import top.charles7c.cnadmin.system.model.vo.UserSocialBindVO; import top.charles7c.cnadmin.system.service.UserService; @@ -106,6 +107,21 @@ public class UserCenterController { return R.ok("修改成功"); } + @Operation(summary = "修改手机号", description = "修改手机号") + @PatchMapping("/phone") + public R updatePhone(@Validated @RequestBody UserPhoneUpdateRequest updateRequest) { + String rawCurrentPassword = + ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateRequest.getCurrentPassword())); + ValidationUtils.throwIfBlank(rawCurrentPassword, "当前密码解密失败"); + String captchaKey = RedisUtils.formatKey(CacheConsts.CAPTCHA_KEY_PREFIX, updateRequest.getNewPhone()); + String captcha = RedisUtils.getCacheObject(captchaKey); + ValidationUtils.throwIfBlank(captcha, "验证码已失效"); + ValidationUtils.throwIfNotEqualIgnoreCase(updateRequest.getCaptcha(), captcha, "验证码错误"); + RedisUtils.deleteCacheObject(captchaKey); + userService.updatePhone(updateRequest.getNewPhone(), rawCurrentPassword, LoginHelper.getUserId()); + return R.ok("修改成功"); + } + @Operation(summary = "修改邮箱", description = "修改用户邮箱") @PatchMapping("/email") public R updateEmail(@Validated @RequestBody UserEmailUpdateRequest updateRequest) { diff --git a/continew-admin-webapi/src/main/resources/config/application-dev.yml b/continew-admin-webapi/src/main/resources/config/application-dev.yml index d04c1d23..8a94a2ad 100644 --- a/continew-admin-webapi/src/main/resources/config/application-dev.yml +++ b/continew-admin-webapi/src/main/resources/config/application-dev.yml @@ -93,6 +93,19 @@ justauth: cache: type: custom +--- ### 短信配置 +sms: + # 从 YAML 读取配置 + config-type: yaml + blends: + cloopen: + # 短信厂商 + supplier: cloopen + base-url: https://app.cloopen.com:8883/2013-12-26 + access-key-id: 你的Access Key + access-key-secret: 你的Access Key Secret + sdk-app-id: 你的应用ID + --- ### 邮件配置 spring.mail: # 根据需要更换 @@ -133,6 +146,16 @@ captcha: limitInSeconds: 60 # 模板路径 templatePath: mail/captcha.ftl + ## 短信验证码配置 + sms: + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 5 + # 限制时间 + limitInSeconds: 60 + # 模板 ID + templateId: 1 --- ### 安全配置-排除路径配置 security.excludes: diff --git a/continew-admin-webapi/src/main/resources/config/application-prod.yml b/continew-admin-webapi/src/main/resources/config/application-prod.yml index ac9b4776..5cfa614f 100644 --- a/continew-admin-webapi/src/main/resources/config/application-prod.yml +++ b/continew-admin-webapi/src/main/resources/config/application-prod.yml @@ -95,6 +95,19 @@ justauth: cache: type: custom +--- ### 短信配置 +sms: + # 从 YAML 读取配置 + config-type: yaml + blends: + cloopen: + # 短信厂商 + supplier: cloopen + base-url: https://app.cloopen.com:8883/2013-12-26 + access-key-id: 你的Access Key + access-key-secret: 你的Access Key Secret + sdk-app-id: 你的应用ID + --- ### 邮件配置 spring.mail: # 根据需要更换 @@ -135,6 +148,16 @@ captcha: limitInSeconds: 60 # 模板路径 templatePath: mail/captcha.ftl + ## 短信验证码配置 + sms: + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 5 + # 限制时间 + limitInSeconds: 60 + # 模板 ID + templateId: 1 --- ### 安全配置-排除路径配置 security.excludes: diff --git a/pom.xml b/pom.xml index 04b9af81..0d15eaa0 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ limitations under the License. 3.9.1 + 3.0.3 1.16.5 3.3.2 2.7.15 @@ -116,6 +117,13 @@ limitations under the License. + + + org.dromara.sms4j + sms4j-spring-boot-starter + ${sms4j.version} + + com.xkcoding.justauth