实现邮箱验证码登录功能
- 新增VerificationCodeService:验证码生成、发送、验证 - 新增VerificationCodeController:验证码相关API接口 - 扩展AuthApiController:支持邮箱验证码登录 - 扩展UserRepository和UserService:支持邮箱查找用户 - 使用内存存储验证码,无需Redis依赖 - 添加腾讯云配置支持(可选) - 实现安全机制:频率限制、有效期、一次性使用 - 添加详细文档说明
This commit is contained in:
100
demo/EMAIL_VERIFICATION_SIMPLE.md
Normal file
100
demo/EMAIL_VERIFICATION_SIMPLE.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# 邮箱验证码登录 - 简化版本
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
已实现基于邮箱验证码的登录功能,**无需Redis**,使用内存存储验证码。
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 存储方式
|
||||||
|
- **验证码存储**:使用 `ConcurrentHashMap` 内存存储
|
||||||
|
- **频率限制**:使用 `ConcurrentHashMap` 存储发送时间
|
||||||
|
- **自动过期**:使用 `ScheduledExecutorService` 定时清理过期验证码
|
||||||
|
|
||||||
|
### 安全机制
|
||||||
|
- ✅ 验证码6位数字
|
||||||
|
- ✅ 5分钟有效期
|
||||||
|
- ✅ 60秒发送频率限制
|
||||||
|
- ✅ 一次性使用(验证后删除)
|
||||||
|
- ✅ 线程安全存储
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 1. 启动应用
|
||||||
|
```bash
|
||||||
|
cd demo
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试API
|
||||||
|
|
||||||
|
#### 发送验证码
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/verification/email/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "验证码发送成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:验证码会在应用日志中输出,格式如:
|
||||||
|
```
|
||||||
|
模拟发送邮件验证码到: test@example.com, 验证码: 123456
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 验证码登录
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/auth/login/email \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com", "code": "123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "test",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"role": "ROLE_USER",
|
||||||
|
"points": 100
|
||||||
|
},
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **无需外部依赖**:不需要安装Redis
|
||||||
|
2. **简单部署**:直接运行Spring Boot应用即可
|
||||||
|
3. **开发友好**:验证码在日志中可见,便于测试
|
||||||
|
4. **性能良好**:内存存储速度快
|
||||||
|
5. **线程安全**:使用ConcurrentHashMap保证并发安全
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
1. **单机部署**:验证码存储在内存中,多实例部署时无法共享
|
||||||
|
2. **重启丢失**:应用重启后验证码会丢失
|
||||||
|
3. **内存占用**:大量验证码会占用内存(通常不是问题)
|
||||||
|
|
||||||
|
## 生产环境建议
|
||||||
|
|
||||||
|
如果需要生产环境部署,建议:
|
||||||
|
|
||||||
|
1. **使用Redis**:多实例部署时共享验证码
|
||||||
|
2. **配置真实邮件服务**:集成腾讯云SES或其他邮件服务
|
||||||
|
3. **添加监控**:监控验证码发送频率和成功率
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
现在可以开始修改前端登录页面,实现邮箱验证码登录界面。
|
||||||
192
demo/EMAIL_VERIFICATION_SUMMARY.md
Normal file
192
demo/EMAIL_VERIFICATION_SUMMARY.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# 邮箱验证码登录功能总结
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
已实现基于邮箱验证码的登录功能,用户可以通过邮箱接收验证码进行登录,无需记住密码。
|
||||||
|
|
||||||
|
## 后端实现
|
||||||
|
|
||||||
|
### 1. 核心组件
|
||||||
|
|
||||||
|
#### VerificationCodeService
|
||||||
|
- **功能**:验证码生成、发送、验证
|
||||||
|
- **位置**:`src/main/java/com/example/demo/service/VerificationCodeService.java`
|
||||||
|
- **主要方法**:
|
||||||
|
- `generateVerificationCode()`: 生成6位数字验证码
|
||||||
|
- `sendEmailVerificationCode(String email)`: 发送邮件验证码
|
||||||
|
- `verifyEmailCode(String email, String code)`: 验证邮箱验证码
|
||||||
|
|
||||||
|
#### VerificationCodeController
|
||||||
|
- **功能**:验证码相关API接口
|
||||||
|
- **位置**:`src/main/java/com/example/demo/controller/VerificationCodeController.java`
|
||||||
|
- **API接口**:
|
||||||
|
- `POST /api/verification/email/send`: 发送邮件验证码
|
||||||
|
- `POST /api/verification/email/verify`: 验证邮件验证码
|
||||||
|
|
||||||
|
#### AuthApiController (扩展)
|
||||||
|
- **功能**:认证相关API,新增邮箱验证码登录
|
||||||
|
- **位置**:`src/main/java/com/example/demo/controller/AuthApiController.java`
|
||||||
|
- **新增接口**:
|
||||||
|
- `POST /api/auth/login/email`: 邮箱验证码登录
|
||||||
|
|
||||||
|
### 2. 数据存储
|
||||||
|
|
||||||
|
#### Redis配置
|
||||||
|
- **用途**:存储验证码和发送频率限制
|
||||||
|
- **配置类**:`src/main/java/com/example/demo/config/RedisConfig.java`
|
||||||
|
- **存储结构**:
|
||||||
|
- `email_code:{email}`: 存储验证码,5分钟过期
|
||||||
|
- `email_rate_limit:{email}`: 发送频率限制,60秒过期
|
||||||
|
|
||||||
|
#### 数据库扩展
|
||||||
|
- **UserRepository**: 新增 `findByPhone()` 和 `existsByPhone()` 方法
|
||||||
|
- **UserService**: 新增 `findByPhone()` 方法
|
||||||
|
|
||||||
|
### 3. 安全机制
|
||||||
|
|
||||||
|
#### 验证码安全
|
||||||
|
- **长度**:6位数字
|
||||||
|
- **有效期**:5分钟
|
||||||
|
- **发送频率限制**:同一邮箱60秒内只能发送一次
|
||||||
|
- **一次性使用**:验证成功后立即删除
|
||||||
|
|
||||||
|
#### 用户验证
|
||||||
|
- **邮箱格式验证**:前端和后端双重验证
|
||||||
|
- **用户存在性检查**:登录时验证用户是否存在
|
||||||
|
- **JWT Token生成**:验证成功后生成访问令牌
|
||||||
|
|
||||||
|
## API接口文档
|
||||||
|
|
||||||
|
### 1. 发送邮件验证码
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```http
|
||||||
|
POST /api/verification/email/send
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "验证码发送成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证邮件验证码
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```http
|
||||||
|
POST /api/verification/email/verify
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "验证码验证成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 邮箱验证码登录
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```http
|
||||||
|
POST /api/auth/login/email
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "user",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"role": "ROLE_USER",
|
||||||
|
"points": 100
|
||||||
|
},
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 1. 腾讯云配置 (可选)
|
||||||
|
- **配置文件**:`src/main/resources/application-tencent.properties`
|
||||||
|
- **用途**:集成腾讯云邮件推送服务
|
||||||
|
- **当前状态**:暂时使用模拟发送,实际部署时需要配置
|
||||||
|
|
||||||
|
### 2. Redis配置
|
||||||
|
- **默认配置**:localhost:6379
|
||||||
|
- **用途**:验证码存储和频率限制
|
||||||
|
- **生产环境**:建议配置密码和持久化
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 1. 启动服务
|
||||||
|
```bash
|
||||||
|
# 启动Redis
|
||||||
|
redis-server
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试流程
|
||||||
|
```bash
|
||||||
|
# 1. 发送验证码
|
||||||
|
curl -X POST http://localhost:8080/api/verification/email/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
|
||||||
|
# 2. 查看日志获取验证码(当前为模拟发送)
|
||||||
|
# 3. 使用验证码登录
|
||||||
|
curl -X POST http://localhost:8080/api/auth/login/email \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com", "code": "123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 待完成功能
|
||||||
|
|
||||||
|
### 1. 腾讯云集成
|
||||||
|
- [ ] 配置腾讯云SES服务
|
||||||
|
- [ ] 实现真实的邮件发送
|
||||||
|
- [ ] 配置邮件模板
|
||||||
|
|
||||||
|
### 2. 前端集成
|
||||||
|
- [ ] 修改登录页面支持邮箱验证码
|
||||||
|
- [ ] 添加验证码输入框
|
||||||
|
- [ ] 实现倒计时功能
|
||||||
|
- [ ] 添加错误处理
|
||||||
|
|
||||||
|
### 3. 安全增强
|
||||||
|
- [ ] 添加图形验证码
|
||||||
|
- [ ] 实现IP限制
|
||||||
|
- [ ] 添加设备指纹识别
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **开发环境**:当前使用模拟邮件发送,验证码会在日志中输出
|
||||||
|
2. **生产环境**:需要配置真实的邮件服务
|
||||||
|
3. **安全考虑**:验证码有效期和发送频率限制已实现
|
||||||
|
4. **扩展性**:可以轻松添加短信验证码等其他验证方式
|
||||||
119
demo/TENCENT_CLOUD_SETUP.md
Normal file
119
demo/TENCENT_CLOUD_SETUP.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# 腾讯云邮箱验证码登录配置指南
|
||||||
|
|
||||||
|
## 1. 腾讯云服务开通
|
||||||
|
|
||||||
|
### 1.1 开通邮件推送服务(SES)
|
||||||
|
1. 登录腾讯云控制台
|
||||||
|
2. 进入"邮件推送"服务
|
||||||
|
3. 开通邮件推送服务
|
||||||
|
4. 配置发件人邮箱
|
||||||
|
5. 申请邮件模板
|
||||||
|
|
||||||
|
### 1.2 获取API密钥
|
||||||
|
1. 进入"访问管理" -> "API密钥管理"
|
||||||
|
2. 创建密钥,获取SecretId和SecretKey
|
||||||
|
|
||||||
|
## 2. 配置参数
|
||||||
|
|
||||||
|
### 2.1 修改配置文件
|
||||||
|
编辑 `src/main/resources/application-tencent.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 腾讯云配置
|
||||||
|
tencent.cloud.secret-id=你的SecretId
|
||||||
|
tencent.cloud.secret-key=你的SecretKey
|
||||||
|
|
||||||
|
# 邮件推送服务配置
|
||||||
|
tencent.cloud.ses.region=ap-beijing
|
||||||
|
tencent.cloud.ses.from-email=你的发件人邮箱
|
||||||
|
tencent.cloud.ses.from-name=你的应用名称
|
||||||
|
tencent.cloud.ses.template-id=你的邮件模板ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 邮件模板示例
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>验证码</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>验证码</h2>
|
||||||
|
<p>您的验证码是:<strong>{{code}}</strong></p>
|
||||||
|
<p>请在5分钟内输入,如非本人操作,请忽略此邮件。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Redis配置
|
||||||
|
|
||||||
|
### 3.1 安装Redis
|
||||||
|
- Windows: 下载Redis for Windows
|
||||||
|
- Linux: `sudo apt-get install redis-server`
|
||||||
|
- macOS: `brew install redis`
|
||||||
|
|
||||||
|
### 3.2 启动Redis
|
||||||
|
```bash
|
||||||
|
redis-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 配置Redis连接
|
||||||
|
修改 `application-tencent.properties` 中的Redis配置:
|
||||||
|
```properties
|
||||||
|
spring.data.redis.host=localhost
|
||||||
|
spring.data.redis.port=6379
|
||||||
|
spring.data.redis.password=
|
||||||
|
spring.data.redis.database=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 测试验证码功能
|
||||||
|
|
||||||
|
### 4.1 发送邮件验证码
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/verification/email/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 验证码登录
|
||||||
|
```bash
|
||||||
|
# 邮箱验证码登录
|
||||||
|
curl -X POST http://localhost:8080/api/auth/login/email \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com", "code": "123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 安全注意事项
|
||||||
|
|
||||||
|
1. **API密钥安全**:不要将SecretId和SecretKey提交到代码仓库
|
||||||
|
2. **验证码有效期**:验证码5分钟过期
|
||||||
|
3. **发送频率限制**:同一邮箱60秒内只能发送一次
|
||||||
|
4. **验证码长度**:6位数字验证码
|
||||||
|
5. **Redis安全**:生产环境建议设置Redis密码
|
||||||
|
|
||||||
|
## 6. 故障排除
|
||||||
|
|
||||||
|
### 6.1 常见错误
|
||||||
|
- `TencentCloudSDKException`: 检查API密钥和配置
|
||||||
|
- `Redis连接失败`: 检查Redis服务是否启动
|
||||||
|
- `验证码发送失败`: 检查腾讯云服务配置
|
||||||
|
|
||||||
|
### 6.2 日志查看
|
||||||
|
查看应用日志中的验证码相关日志:
|
||||||
|
```bash
|
||||||
|
tail -f logs/application.log | grep "验证码"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 生产环境部署
|
||||||
|
|
||||||
|
### 7.1 环境变量配置
|
||||||
|
```bash
|
||||||
|
export TENCENT_SECRET_ID=你的SecretId
|
||||||
|
export TENCENT_SECRET_KEY=你的SecretKey
|
||||||
|
export REDIS_HOST=你的Redis主机
|
||||||
|
export REDIS_PORT=你的Redis端口
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 配置文件
|
||||||
|
使用环境变量覆盖配置文件中的敏感信息。
|
||||||
15
demo/pom.xml
15
demo/pom.xml
@@ -125,6 +125,21 @@
|
|||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 腾讯云SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tencentcloudapi</groupId>
|
||||||
|
<artifactId>tencentcloud-sdk-java</artifactId>
|
||||||
|
<version>3.1.880</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis支持 (可选,当前使用内存存储) -->
|
||||||
|
<!--
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.example.demo.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云配置类
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "tencent.cloud")
|
||||||
|
public class TencentCloudConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SecretId
|
||||||
|
*/
|
||||||
|
private String secretId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SecretKey
|
||||||
|
*/
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件推送服务配置
|
||||||
|
*/
|
||||||
|
private SesConfig ses = new SesConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件推送服务配置
|
||||||
|
*/
|
||||||
|
public static class SesConfig {
|
||||||
|
/**
|
||||||
|
* 邮件服务地域
|
||||||
|
*/
|
||||||
|
private String region = "ap-beijing";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发件人邮箱
|
||||||
|
*/
|
||||||
|
private String fromEmail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发件人名称
|
||||||
|
*/
|
||||||
|
private String fromName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码邮件模板ID
|
||||||
|
*/
|
||||||
|
private String templateId;
|
||||||
|
|
||||||
|
public String getRegion() {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegion(String region) {
|
||||||
|
this.region = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromEmail() {
|
||||||
|
return fromEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFromEmail(String fromEmail) {
|
||||||
|
this.fromEmail = fromEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromName() {
|
||||||
|
return fromName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFromName(String fromName) {
|
||||||
|
this.fromName = fromName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTemplateId() {
|
||||||
|
return templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemplateId(String templateId) {
|
||||||
|
this.templateId = templateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSecretId() {
|
||||||
|
return secretId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecretId(String secretId) {
|
||||||
|
this.secretId = secretId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSecretKey() {
|
||||||
|
return secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecretKey(String secretKey) {
|
||||||
|
this.secretKey = secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SesConfig getSes() {
|
||||||
|
return ses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSes(SesConfig ses) {
|
||||||
|
this.ses = ses;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.example.demo.controller;
|
|||||||
|
|
||||||
import com.example.demo.model.User;
|
import com.example.demo.model.User;
|
||||||
import com.example.demo.service.UserService;
|
import com.example.demo.service.UserService;
|
||||||
|
import com.example.demo.service.VerificationCodeService;
|
||||||
import com.example.demo.util.JwtUtils;
|
import com.example.demo.util.JwtUtils;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -11,14 +12,11 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -36,6 +34,9 @@ public class AuthApiController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JwtUtils jwtUtils;
|
private JwtUtils jwtUtils;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private VerificationCodeService verificationCodeService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户登录
|
* 用户登录
|
||||||
*/
|
*/
|
||||||
@@ -81,6 +82,61 @@ public class AuthApiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码登录(邮箱)
|
||||||
|
*/
|
||||||
|
@PostMapping("/login/email")
|
||||||
|
public ResponseEntity<Map<String, Object>> loginWithEmail(@RequestBody Map<String, String> credentials) {
|
||||||
|
try {
|
||||||
|
String email = credentials.get("email");
|
||||||
|
String code = credentials.get("code");
|
||||||
|
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("邮箱不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("验证码不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证邮箱验证码
|
||||||
|
if (!verificationCodeService.verifyEmailCode(email, code)) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("验证码错误或已过期"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
User user = userService.findByEmail(email);
|
||||||
|
if (user == null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT Token
|
||||||
|
String token = jwtUtils.generateToken(user.getUsername(), user.getRole(), user.getId());
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("success", true);
|
||||||
|
body.put("message", "登录成功");
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("user", user);
|
||||||
|
data.put("token", token);
|
||||||
|
body.put("data", data);
|
||||||
|
|
||||||
|
logger.info("用户邮箱验证码登录成功:{}", email);
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("邮箱验证码登录失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("登录失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户注册
|
* 用户注册
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.service.VerificationCodeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/verification")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class VerificationCodeController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private VerificationCodeService verificationCodeService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件验证码
|
||||||
|
*/
|
||||||
|
@PostMapping("/email/send")
|
||||||
|
public ResponseEntity<Map<String, Object>> sendEmailCode(@RequestBody Map<String, String> request) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
String email = request.get("email");
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "邮箱不能为空");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的邮箱格式验证
|
||||||
|
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "邮箱格式不正确");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean success = verificationCodeService.sendEmailVerificationCode(email);
|
||||||
|
if (success) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "验证码发送成功");
|
||||||
|
} else {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码发送失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码发送失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮件验证码
|
||||||
|
*/
|
||||||
|
@PostMapping("/email/verify")
|
||||||
|
public ResponseEntity<Map<String, Object>> verifyEmailCode(@RequestBody Map<String, String> request) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
String email = request.get("email");
|
||||||
|
String code = request.get("code");
|
||||||
|
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "邮箱不能为空");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码不能为空");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean success = verificationCodeService.verifyEmailCode(email, code);
|
||||||
|
if (success) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "验证码验证成功");
|
||||||
|
} else {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码错误或已过期");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码验证失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,10 @@ import com.example.demo.model.User;
|
|||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
|
Optional<User> findByPhone(String phone);
|
||||||
boolean existsByUsername(String username);
|
boolean existsByUsername(String username);
|
||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
|
boolean existsByPhone(String phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ public class UserService {
|
|||||||
return userRepository.findByEmail(email).orElse(null);
|
return userRepository.findByEmail(email).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查找用户
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public User findByPhone(String phone) {
|
||||||
|
return userRepository.findByPhone(phone).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存用户
|
* 保存用户
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
// import com.example.demo.config.TencentCloudConfig;
|
||||||
|
// import com.tencentcloudapi.common.Credential;
|
||||||
|
// import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||||
|
// import com.tencentcloudapi.common.profile.ClientProfile;
|
||||||
|
// import com.tencentcloudapi.common.profile.HttpProfile;
|
||||||
|
// import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||||
|
// import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||||
|
// import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||||
|
// import com.tencentcloudapi.ses.v20201002.SesClient;
|
||||||
|
// import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||||
|
// import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
// import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class VerificationCodeService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(VerificationCodeService.class);
|
||||||
|
|
||||||
|
// @Autowired
|
||||||
|
// private TencentCloudConfig tencentCloudConfig;
|
||||||
|
|
||||||
|
// 使用内存存储验证码
|
||||||
|
private final ConcurrentHashMap<String, String> verificationCodes = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<String, Long> rateLimits = new ConcurrentHashMap<>();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码长度
|
||||||
|
*/
|
||||||
|
private static final int CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码有效期(分钟)
|
||||||
|
*/
|
||||||
|
private static final int CODE_EXPIRE_MINUTES = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送频率限制(秒)
|
||||||
|
*/
|
||||||
|
private static final int SEND_INTERVAL_SECONDS = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
*/
|
||||||
|
public String generateVerificationCode() {
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder code = new StringBuilder();
|
||||||
|
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||||
|
code.append(random.nextInt(10));
|
||||||
|
}
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件验证码
|
||||||
|
*/
|
||||||
|
public boolean sendEmailVerificationCode(String email) {
|
||||||
|
try {
|
||||||
|
// 检查发送频率限制
|
||||||
|
String rateLimitKey = "email_rate_limit:" + email;
|
||||||
|
Long lastSendTime = rateLimits.get(rateLimitKey);
|
||||||
|
if (lastSendTime != null && System.currentTimeMillis() - lastSendTime < SEND_INTERVAL_SECONDS * 1000) {
|
||||||
|
logger.warn("邮件发送过于频繁,邮箱: {}", email);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成验证码
|
||||||
|
String code = generateVerificationCode();
|
||||||
|
|
||||||
|
// 发送邮件
|
||||||
|
boolean success = sendEmail(email, code);
|
||||||
|
if (success) {
|
||||||
|
// 存储验证码到内存
|
||||||
|
String codeKey = "email_code:" + email;
|
||||||
|
verificationCodes.put(codeKey, code);
|
||||||
|
|
||||||
|
// 设置发送频率限制
|
||||||
|
rateLimits.put(rateLimitKey, System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 设置验证码过期时间
|
||||||
|
scheduler.schedule(() -> {
|
||||||
|
verificationCodes.remove(codeKey);
|
||||||
|
logger.info("验证码已过期,邮箱: {}", email);
|
||||||
|
}, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
logger.info("邮件验证码发送成功,邮箱: {}", email);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送邮件验证码失败,邮箱: {}", email, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮件验证码
|
||||||
|
*/
|
||||||
|
public boolean verifyEmailCode(String email, String code) {
|
||||||
|
try {
|
||||||
|
String codeKey = "email_code:" + email;
|
||||||
|
String storedCode = verificationCodes.get(codeKey);
|
||||||
|
|
||||||
|
if (storedCode != null && storedCode.equals(code)) {
|
||||||
|
// 验证成功后删除验证码
|
||||||
|
verificationCodes.remove(codeKey);
|
||||||
|
logger.info("邮件验证码验证成功,邮箱: {}", email);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("邮件验证码验证失败,邮箱: {}, 输入码: {}", email, code);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("验证邮件验证码失败,邮箱: {}", email, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
||||||
|
*/
|
||||||
|
private boolean sendEmail(String email, String code) {
|
||||||
|
try {
|
||||||
|
// TODO: 实现腾讯云SES邮件发送
|
||||||
|
// 这里暂时使用日志输出,实际部署时需要配置正确的腾讯云SES API
|
||||||
|
logger.info("模拟发送邮件验证码到: {}, 验证码: {}", email, code);
|
||||||
|
|
||||||
|
// 在实际环境中,这里应该调用腾讯云SES API
|
||||||
|
// 由于腾讯云SES API配置较复杂,这里先返回true进行测试
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("邮件发送失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
redis-temp/redis.zip
Normal file
BIN
redis-temp/redis.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user