diff --git a/han-note-auth/pom.xml b/han-note-auth/pom.xml index 85faf5d..fbd9110 100755 --- a/han-note-auth/pom.xml +++ b/han-note-auth/pom.xml @@ -42,6 +42,30 @@ com.hanserwei hanserwei-spring-boot-starter-jackson + + + cn.dev33 + sa-token-spring-boot3-starter + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.commons + commons-pool2 + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-thymeleaf + + diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/RedisTemplateConfig.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/RedisTemplateConfig.java new file mode 100644 index 0000000..c931d20 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/RedisTemplateConfig.java @@ -0,0 +1,31 @@ +package com.hanserwei.hannote.auth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 设置 RedisTemplate 的连接工厂 + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + + // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + redisTemplate.setValueSerializer(serializer); + redisTemplate.setHashValueSerializer(serializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/ThreadPoolConfig.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/ThreadPoolConfig.java new file mode 100644 index 0000000..9ebe8d3 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/config/ThreadPoolConfig.java @@ -0,0 +1,35 @@ +package com.hanserwei.hannote.auth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration +public class ThreadPoolConfig { + @Bean(name = "authTaskExecutor") + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程数 + executor.setCorePoolSize(10); + //最大线程数 + executor.setMaxPoolSize(50); + //队列容量 + executor.setQueueCapacity(200); + //活跃时间 + executor.setKeepAliveSeconds(30); + //线程名前缀 + executor.setThreadNamePrefix("AuthExecutor-"); + + //拒绝策略:由调用线程处理,一般为主线程 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 等待所有任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + // 设置等待时间,如果超过这个时间还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞 + executor.setAwaitTerminationSeconds(60); + + executor.initialize(); + return executor; + } +} diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/constant/RedisKeyConstants.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/constant/RedisKeyConstants.java new file mode 100644 index 0000000..859e5cd --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/constant/RedisKeyConstants.java @@ -0,0 +1,18 @@ +package com.hanserwei.hannote.auth.constant; + +public class RedisKeyConstants { + + /** + * 验证码 KEY 前缀 + */ + private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:"; + + /** + * 构建验证码 KEY + * @param email 手机号 + * @return 验证码key + */ + public static String buildVerificationCodeKey(String email) { + return VERIFICATION_CODE_KEY_PREFIX + email; + } +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/VerificationCodeController.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/VerificationCodeController.java new file mode 100644 index 0000000..d892372 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/controller/VerificationCodeController.java @@ -0,0 +1,26 @@ +package com.hanserwei.hannote.auth.controller; + +import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog; +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO; +import com.hanserwei.hannote.auth.service.VerificationCodeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +@RequiredArgsConstructor +public class VerificationCodeController { + + private final VerificationCodeService verificationCodeService; + + @PostMapping("/verification/code/send") + @ApiOperationLog(description = "发送邮件验证码") + public Response send(@Validated @RequestBody SendVerificationCodeReqVO sendVerificationCodeReqVO) { + return verificationCodeService.send(sendVerificationCodeReqVO); + } +} diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java index 1b377d2..f04237f 100644 --- a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/enums/ResponseCodeEnum.java @@ -13,6 +13,9 @@ public enum ResponseCodeEnum implements BaseExceptionInterface { PARAM_NOT_VALID("AUTH-10001", "参数错误!!!"), // ----------- 业务异常状态码 ----------- + VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"), + MAIL_SEND_ERROR("AUTH-20001", "邮件发送失败,请稍后再试"), + TEMPLATE_RENDER_ERROR("AUTH-20002", "模板渲染错误") ; // 异常码 diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/SendVerificationCodeReqVO.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/SendVerificationCodeReqVO.java new file mode 100644 index 0000000..725504e --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/model/vo/SendVerificationCodeReqVO.java @@ -0,0 +1,18 @@ +package com.hanserwei.hannote.auth.model.vo; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SendVerificationCodeReqVO { + + @NotBlank(message = "邮箱不能为空") + private String email; + +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/VerificationCodeService.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/VerificationCodeService.java new file mode 100644 index 0000000..d88ef7d --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/VerificationCodeService.java @@ -0,0 +1,15 @@ +package com.hanserwei.hannote.auth.service; + +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO; + +public interface VerificationCodeService { + + /** + * 发送短信验证码 + * + * @param sendVerificationCodeReqVO 发送验证码VO + * @return 返回响应 + */ + Response send(SendVerificationCodeReqVO sendVerificationCodeReqVO); +} \ No newline at end of file diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/VerificationCodeServiceImpl.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/VerificationCodeServiceImpl.java new file mode 100644 index 0000000..7258df3 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/service/impl/VerificationCodeServiceImpl.java @@ -0,0 +1,57 @@ +package com.hanserwei.hannote.auth.service.impl; + +import cn.hutool.core.util.RandomUtil; +import com.hanserwei.framework.common.exception.ApiException; +import com.hanserwei.framework.common.response.Response; +import com.hanserwei.hannote.auth.constant.RedisKeyConstants; +import com.hanserwei.hannote.auth.enums.ResponseCodeEnum; +import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO; +import com.hanserwei.hannote.auth.service.VerificationCodeService; +import com.hanserwei.hannote.auth.utils.MailHelper; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Service +@Slf4j +public class VerificationCodeServiceImpl implements VerificationCodeService { + + + private final RedisTemplate redisTemplate; + private final MailHelper mailHelper; + @Resource(name = "authTaskExecutor") + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + /** + * 发送短信验证码! + * + * @param sendVerificationCodeReqVO 发送验证码VO + * @return 响应 + */ + @Override + public Response send(SendVerificationCodeReqVO sendVerificationCodeReqVO) { + // 邮箱 + String email = sendVerificationCodeReqVO.getEmail(); + //构建Redis的Key + String codeKey = RedisKeyConstants.buildVerificationCodeKey(email); + // 判断是否发送! + Boolean hasKey = redisTemplate.hasKey(codeKey); + if (hasKey) { + //若之前发送的验证码未过期,则提示发送频繁 + throw new ApiException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY); + } + //生成六位数随机验证码 + String verificationCode = RandomUtil.randomNumbers(6); + threadPoolTaskExecutor.submit(() -> mailHelper.sendMail(verificationCode, email)); + log.info("==> 邮箱: {}, 已发送验证码:【{}】", email, verificationCode); + // 存储验证码到 redis, 并设置过期时间为 3 分钟 + redisTemplate.opsForValue().set(codeKey, verificationCode, 3, TimeUnit.MINUTES); + return Response.success(); + } +} diff --git a/han-note-auth/src/main/java/com/hanserwei/hannote/auth/utils/MailHelper.java b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/utils/MailHelper.java new file mode 100644 index 0000000..7ad2178 --- /dev/null +++ b/han-note-auth/src/main/java/com/hanserwei/hannote/auth/utils/MailHelper.java @@ -0,0 +1,66 @@ +package com.hanserwei.hannote.auth.utils; + +import com.hanserwei.framework.common.exception.ApiException; +import com.hanserwei.hannote.auth.enums.ResponseCodeEnum; +import jakarta.annotation.Resource; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.util.Arrays; +import java.util.Date; + +@Slf4j +@Component +public class MailHelper { + + @Resource + private JavaMailSender mailSender; + + @Resource + private TemplateEngine templateEngine; + + @Value("${spring.mail.username}") + private String username; + + + public boolean sendMail(String verificationCode, String email) { + Context context = new Context(); + context.setVariable("verifyCode", Arrays.asList(verificationCode.split(""))); + + String process; + try { + // 确保这里的 templateEngine 能够正确处理 context + process = templateEngine.process("EmailVerificationCode.html", context); + } catch (Exception e) { + // 处理模板渲染失败的异常 + throw new ApiException(ResponseCodeEnum.TEMPLATE_RENDER_ERROR); + } + + MimeMessage mimeMessage = mailSender.createMimeMessage(); + + try { + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setSubject("【Han-note】验证码"); + helper.setFrom(username); + helper.setTo(email); + helper.setSentDate(new Date()); + helper.setText(process, true); + + mailSender.send(mimeMessage); // 可能会抛出 MailException (RuntimeException) + log.info("邮件发送成功!"); + } catch (MessagingException | org.springframework.mail.MailException e) { + // 捕获 MimeMessageHelper 配置异常 和 mailSender 发送异常 + log.error("邮件发送失败:{}", e.getMessage()); + throw new ApiException(ResponseCodeEnum.MAIL_SEND_ERROR); + } + return true; + } + +} diff --git a/han-note-auth/src/main/resources/application.yml b/han-note-auth/src/main/resources/application.yml index 2c319f9..ec21a96 100755 --- a/han-note-auth/src/main/resources/application.yml +++ b/han-note-auth/src/main/resources/application.yml @@ -9,6 +9,29 @@ spring: url: ${spring.datasource.url} username: ${spring.datasource.username} # 数据库用户名 password: ${spring.datasource.password} # 数据库密码 + data: + redis: + database: 5 # Redis 数据库索引(默认为 0) + host: 127.0.0.1 # Redis 服务器地址 + port: 6379 # Redis 服务器连接端口 + password: redis # Redis 服务器连接密码(默认为空) + timeout: 5s # 读超时时间 + connect-timeout: 5s # 链接超时时间 + lettuce: + pool: + max-active: 200 # 连接池最大连接数 + max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) + min-idle: 0 # 连接池中的最小空闲连接 + max-idle: 10 # 连接池中的最大空闲连接 + mail: + host: smtp.qq.com + port: 587 + username: 2628273921@qq.com + password: ${MAIL_PASSWORD:} + properties: + mail.smtp.auth: true + mail.smtp.starttls.enable: true + mail.smtp.starttls.required: true server: port: 8080 # 项目启动的端口 mybatis-plus: @@ -21,3 +44,19 @@ mybatis-plus: logging: level: com.hanserwei.hannote.auth.domain.mapper: debug +############## Sa-Token 配置 (文档: https://sa-token.cc) ############## +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: true + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: simple-uuid + # 是否输出操作日志 + is-log: true diff --git a/han-note-auth/src/main/resources/templates/EmailVerificationCode.html b/han-note-auth/src/main/resources/templates/EmailVerificationCode.html new file mode 100644 index 0000000..5551ae3 --- /dev/null +++ b/han-note-auth/src/main/resources/templates/EmailVerificationCode.html @@ -0,0 +1,169 @@ + + + + + 邮箱验证码 + + + + + + + + + +
+
+ + + + + + +
+
+ +
+
+ 尊敬的用户,您好! + + 您正在使用验证码校验,请在5分钟内填写如下验证码,如非本人操作请忽略该邮件 + +
+ +
+
+
+ + 注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全 +
(工作人员不会向你索取此验证码,请勿泄漏!) +
+
+
+
+
+

此为系统邮件,请勿回复
+ 请保管好您的邮箱,避免账号被他人盗用 +

+

——Han-note官方

+
+
+
+ diff --git a/han-note-auth/src/test/java/com/hanserwei/hannote/auth/RedisTests.java b/han-note-auth/src/test/java/com/hanserwei/hannote/auth/RedisTests.java new file mode 100644 index 0000000..c4a8b7d --- /dev/null +++ b/han-note-auth/src/test/java/com/hanserwei/hannote/auth/RedisTests.java @@ -0,0 +1,49 @@ +package com.hanserwei.hannote.auth; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +@SpringBootTest +@Slf4j +class RedisTests { + + @Resource + private RedisTemplate redisTemplate; + + /** + * set key value + */ + @Test + void testSetKeyValue() { + // 添加一个 key 为 name, value 值为 Hanserwei + redisTemplate.opsForValue().set("name", "Hanserwei"); + } + + /** + * 判断某个 key 是否存在 + */ + @Test + void testHasKey() { + log.info("key 是否存在:{}", redisTemplate.hasKey("name")); + } + + /** + * 获取某个 key 的 value + */ + @Test + void testGetValue() { + log.info("value 值:{}", redisTemplate.opsForValue().get("name")); + } + + /** + * 删除某个 key + */ + @Test + void testDelete() { + redisTemplate.delete("name"); + } + +} \ No newline at end of file diff --git a/han-note-auth/src/test/java/com/hanserwei/hannote/auth/ThreadPoolTaskExecutorTests.java b/han-note-auth/src/test/java/com/hanserwei/hannote/auth/ThreadPoolTaskExecutorTests.java new file mode 100644 index 0000000..e82a62e --- /dev/null +++ b/han-note-auth/src/test/java/com/hanserwei/hannote/auth/ThreadPoolTaskExecutorTests.java @@ -0,0 +1,23 @@ +package com.hanserwei.hannote.auth; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@SpringBootTest +@Slf4j +public class ThreadPoolTaskExecutorTests { + + @Resource + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + /** + * 测试线程池 + */ + @Test + void testSubmit() { + threadPoolTaskExecutor.submit(() -> log.info("异步线程中说: Hanserwei是傻逼")); + } +} \ No newline at end of file diff --git a/hanserwei-framework/hanserwei-common/pom.xml b/hanserwei-framework/hanserwei-common/pom.xml index 53dfaf1..45de213 100755 --- a/hanserwei-framework/hanserwei-common/pom.xml +++ b/hanserwei-framework/hanserwei-common/pom.xml @@ -45,5 +45,18 @@ org.hibernate.validator hibernate-validator + + + com.google.guava + guava + + + cn.hutool + hutool-all + + + org.apache.commons + commons-lang3 + diff --git a/pom.xml b/pom.xml index a1fae68..0b414d9 100755 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,10 @@ 3.5.14 8.4.0 1.2.27 + 1.44.0 + 33.5.0-jre + 5.8.40 + 3.19.0 @@ -113,6 +117,28 @@ druid-spring-boot-3-starter ${druid.version} + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + com.google.guava + guava + ${guava.version} + + + cn.hutool + hutool-all + ${hutool.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + diff --git a/sql/createTable.sql b/sql/createTable.sql new file mode 100644 index 0000000..9368679 --- /dev/null +++ b/sql/createTable.sql @@ -0,0 +1,20 @@ +CREATE TABLE `t_user` +( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `han_note_id` varchar(15) NOT NULL COMMENT '小哈书号(唯一凭证)', + `password` varchar(64) DEFAULT NULL COMMENT '密码', + `nickname` varchar(24) NOT NULL COMMENT '昵称', + `avatar` varchar(120) DEFAULT NULL COMMENT '头像', + `birthday` date DEFAULT NULL COMMENT '生日', + `background_img` varchar(120) DEFAULT NULL COMMENT '背景图', + `phone` varchar(11) NOT NULL COMMENT '手机号', + `sex` tinyint DEFAULT '0' COMMENT '性别(0:女 1:男)', + `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0:启用 1:禁用)', + `introduction` varchar(100) DEFAULT NULL COMMENT '个人简介', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', + `is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_han_note_id` (`han_note_id`), + UNIQUE KEY `uk_phone` (`phone`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';