# 短信验证系统使用指南

**版本：** v1.0.0  
**更新时间：** 2025-11-03  
**系统名称：** 1818AI 用户服务短信验证模块

---

## 📋 目录

- [系统概述](#系统概述)
- [配置说明](#配置说明)
- [API接口文档](#api接口文档)
- [业务场景](#业务场景)
- [安全机制](#安全机制)
- [错误处理](#错误处理)
- [使用示例](#使用示例)
- [常见问题](#常见问题)
- [维护指南](#维护指南)

---

## 📖 系统概述

### 功能说明

短信验证系统基于**阿里云短信服务**实现，提供验证码的发送和校验功能。主要用于用户注册、登录、密码重置等关键业务场景。

### 技术架构

```
┌─────────────────────────────────────────────────┐
│                   前端应用                       │
└──────────────────┬──────────────────────────────┘
                   │ HTTP/HTTPS
                   ▼
┌─────────────────────────────────────────────────┐
│              Spring Boot 应用                    │
│  ┌──────────────┐      ┌──────────────┐        │
│  │MsmController │─────▶│ MsmService   │        │
│  └──────────────┘      └──────┬───────┘        │
│         │                      │                 │
│         ▼                      ▼                 │
│  ┌──────────────┐      ┌──────────────┐        │
│  │Redis缓存     │      │阿里云SMS API │        │
│  │(验证码存储)  │      │(短信发送)    │        │
│  └──────────────┘      └──────────────┘        │
└─────────────────────────────────────────────────┘
```

### 核心特性

- ✅ **6位数字验证码** - 简单易输入
- ✅ **5分钟有效期** - 自动过期保护
- ✅ **一次性使用** - 验证后立即失效
- ✅ **防重复发送** - 验证码存在时拒绝重发
- ✅ **强制发送模式** - 支持覆盖已存在的验证码
- ✅ **完整日志记录** - 便于追踪和调试

---

## ⚙️ 配置说明

### 配置文件位置

```
src/main/resources/application.yml
```

### 配置内容

```yaml
# --- 短信配置 ---
ly:
  sms:
    accessKeyId: LTAI5t68do3qVXx5Rufugt3X          # 阿里云AccessKey ID
    accessKeySecret: 2vD9ToIff49Vph4JQXsn0Cy8nXQfzA # 阿里云AccessKey Secret
    signName: 星洋智慧                              # 短信签名
    verifyTemplateCode: SMS_491985030              # 验证码短信模板编号
```

### 配置项说明

| 配置项 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `accessKeyId` | String | 是 | 阿里云访问密钥ID，从阿里云控制台获取 |
| `accessKeySecret` | String | 是 | 阿里云访问密钥Secret，从阿里云控制台获取 |
| `signName` | String | 是 | 短信签名，需在阿里云短信服务中申请 |
| `verifyTemplateCode` | String | 是 | 短信模板编号，需在阿里云短信服务中申请 |

### 阿里云短信服务配置步骤

#### 1. 开通短信服务

1. 登录 [阿里云控制台](https://www.aliyun.com/)
2. 开通"短信服务"产品
3. 完成实名认证

#### 2. 创建短信签名

1. 进入"短信服务控制台" → "国内消息" → "签名管理"
2. 点击"添加签名"
3. 填写签名信息：
   - **签名名称**：星洋智慧（或您的公司/产品名）
   - **签名来源**：企事业单位的全称或简称
   - **适用场景**：验证码
4. 提交审核（通常1-2个工作日）

#### 3. 创建短信模板

1. 进入"模板管理" → "添加模板"
2. 填写模板信息：
   - **模板类型**：验证码
   - **模板名称**：验证码通知
   - **模板内容**：`您的验证码为：${code}，5分钟内有效，请勿泄露给他人。`
3. 提交审核（通常1-2个工作日）
4. 审核通过后获得**模板CODE**（如：SMS_491985030）

#### 4. 获取AccessKey

1. 进入"AccessKey管理"
2. 创建AccessKey（如果没有）
3. 记录 `AccessKey ID` 和 `AccessKey Secret`

⚠️ **安全提示**：
- AccessKey Secret 请妥善保管，不要泄露
- 建议使用子账号AccessKey，并授予最小权限
- 定期轮换AccessKey

---

## 🔌 API接口文档

### 1. 发送短信验证码

#### 接口信息

```
GET /user/msm/send/{phone}
```

#### 请求参数

**路径参数**：

| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| phone | String | 是 | 手机号（11位） | 13800138000 |

**Query参数**：

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| force | Boolean | 否 | false | 是否强制发送新验证码 |

#### 响应示例

**成功响应**：

```json
{
  "code": 200,
  "message": "success",
  "data": true
}
```

**失败响应**：

```json
{
  "code": 400,
  "message": "验证码已存在，请稍后再试",
  "data": null
}
```

```json
{
  "code": 500,
  "message": "短信发送失败，请稍后重试",
  "data": null
}
```

#### 业务逻辑

```
1. 检查Redis中是否已有验证码
   ├─ 有验证码 且 force=false → 返回错误（400）
   └─ 无验证码 或 force=true → 继续
       ↓
2. 生成6位随机数字验证码（100000-999999）
       ↓
3. 调用阿里云短信API发送验证码
       ↓
4. 发送成功
   ├─ 是 → 存入Redis（5分钟过期）→ 返回成功（200）
   └─ 否 → 返回错误（500）
```

#### 使用示例

**普通发送**：

```bash
curl -X GET "http://localhost:8081/user/msm/send/13800138000"
```

**强制发送**（覆盖已存在的验证码）：

```bash
curl -X GET "http://localhost:8081/user/msm/send/13800138000?force=true"
```

#### 注意事项

- ⏱️ 验证码5分钟内有效
- 🔒 同一手机号在验证码未过期前不能重复发送（除非force=true）
- 💰 每次发送会产生短信费用
- 📝 所有操作都会记录详细日志

---

## 💼 业务场景

### 1. 短信登录

**接口**：`POST /user/auth/sms-login`

**流程**：

```
用户输入手机号
    ↓
调用发送验证码接口
    ↓
用户收到短信
    ↓
用户输入验证码
    ↓
调用登录接口（验证码校验）
    ↓
校验成功 → 生成JWT Token → 返回登录成功
```

**请求示例**：

```json
{
  "phone": "13800138000",
  "code": "123456"
}
```

**响应示例**：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiJ9...",
    "tokenExpiresAt": "2025-11-10T12:00:00",
    "userInfo": {
      "userId": 123456,
      "phone": "13800138000",
      "username": "用户昵称"
    }
  }
}
```

### 2. 用户注册

**接口**：`POST /user/auth/register`

**流程**：

```
用户输入手机号和密码
    ↓
调用发送验证码接口
    ↓
用户输入验证码
    ↓
调用注册接口（验证码校验）
    ↓
校验成功 → 创建用户 → 自动登录 → 返回Token
```

**请求示例**：

```json
{
  "phone": "13800138000",
  "code": "123456",
  "password": "Abc123456",
  "inviteCode": "ABC123" // 可选
}
```

### 3. 重置密码

**接口**：`POST /user/auth/reset-password`

**流程**：

```
用户输入手机号
    ↓
调用发送验证码接口
    ↓
用户输入验证码和新密码
    ↓
调用重置密码接口（验证码校验）
    ↓
校验成功 → 更新密码 → 返回成功
```

**请求示例**：

```json
{
  "phone": "13800138000",
  "code": "123456",
  "newPassword": "NewPass123"
}
```

### 4. 修改手机号

**接口**：`PUT /user/users/info`

**流程**：

```
用户输入新手机号
    ↓
向新手机号发送验证码
    ↓
用户输入验证码
    ↓
调用修改接口（验证码校验）
    ↓
校验成功 → 更新手机号 → 返回成功
```

### 5. 修改密码（需要验证码）

**接口**：`PUT /user/users/info`

**流程**：

```
用户输入当前手机号
    ↓
发送验证码
    ↓
用户输入验证码和新密码
    ↓
调用修改接口（验证码校验）
    ↓
校验成功 → 更新密码 → 返回成功
```

---

## 🔒 安全机制

### 1. 验证码有效期控制

```java
// 存储到Redis，5分钟后自动过期
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
```

**说明**：
- 验证码在Redis中存储5分钟
- 5分钟后自动删除，无法继续使用
- 防止验证码长期有效带来的安全风险

### 2. 一次性使用机制

```java
// 验证成功后立即删除
redisTemplate.delete(phone);
```

**说明**：
- 验证码验证成功后立即从Redis删除
- 每个验证码只能使用一次
- 防止验证码被重复使用

### 3. 错误清除机制

```java
// 验证失败时清除验证码
if (!cachedCode.equals(code)) {
    redisTemplate.delete(phone);
    throw new RuntimeException("验证码错误或已过期");
}
```

**说明**：
- 验证码输入错误时立即清除
- 防止暴力破解攻击
- 用户需重新获取验证码

### 4. 业务失败清除

```java
// 业务逻辑失败时也清除验证码
if (existingUser != null) {
    redisTemplate.delete(phone);
    throw new RuntimeException("手机号已注册");
}
```

**说明**：
- 即使验证码正确，但业务逻辑失败时也清除验证码
- 例如：注册时手机号已存在、登录时用户不存在等
- 确保一个验证码只用于一次完整的业务操作

### 5. 重复发送限制

```java
// 检查是否已有验证码
String existingCode = redisTemplate.opsForValue().get(phone);
if (existingCode != null && !force) {
    return Result.error(400, "验证码已存在，请稍后再试");
}
```

**说明**：
- 验证码存在时拒绝重复发送
- 防止短信轰炸和资源浪费
- 特殊情况可使用`force=true`强制发送

### 安全建议

#### ⚠️ 当前缺少的安全措施

1. **频率限制**
   ```
   建议：同一手机号1分钟内最多发送1次
   建议：同一IP每小时最多发送10次
   建议：单个手机号每天最多发送5次
   ```

2. **图形验证码**
   ```
   建议：发送短信前先验证图形验证码
   防止：自动化机器人攻击
   ```

3. **手机号归属地验证**
   ```
   建议：检查手机号归属地是否为国内
   防止：国际短信费用损失
   ```

4. **黑名单机制**
   ```
   建议：维护恶意手机号黑名单
   防止：滥用和攻击
   ```

---

## ❌ 错误处理

### 错误码说明

| 错误码 | 错误信息 | 原因 | 解决方法 |
|--------|---------|------|----------|
| 400 | 验证码已存在，请稍后再试 | Redis中已有未过期的验证码 | 等待5分钟或使用force=true |
| 400 | 验证码错误或已过期 | 验证码不存在或不匹配 | 重新获取验证码 |
| 500 | 短信发送失败，请稍后重试 | 阿里云短信服务调用失败 | 检查配置和网络，查看日志 |
| 500 | 发送短信验证码失败 | 系统异常 | 查看服务器日志 |

### 阿里云短信服务错误码

| 阿里云错误码 | 说明 | 处理方法 |
|-------------|------|----------|
| OK | 发送成功 | - |
| isv.MOBILE_NUMBER_ILLEGAL | 手机号格式错误 | 检查手机号格式 |
| isv.BUSINESS_LIMIT_CONTROL | 业务限流 | 降低发送频率 |
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 充值阿里云短信服务 |
| isv.TEMPLATE_MISSING_PARAMETERS | 模板参数缺失 | 检查模板参数 |
| isv.INVALID_PARAMETERS | 参数无效 | 检查所有参数 |

### 日志查询

```bash
# 查看短信发送日志
sudo journalctl -u spring_1818_user_server | grep "短信"

# 查看特定手机号的日志
sudo journalctl -u spring_1818_user_server | grep "13800138000"

# 查看错误日志
sudo journalctl -u spring_1818_user_server | grep -E "ERROR|WARN" | grep "短信"
```

---

## 📘 使用示例

### 完整的登录流程示例

#### 步骤1：发送验证码

```bash
curl -X GET "http://localhost:8081/user/msm/send/13800138000" \
  -H "Accept: application/json"
```

**响应**：
```json
{
  "code": 200,
  "message": "success",
  "data": true
}
```

#### 步骤2：用户收到短信

```
【星洋智慧】您的验证码为：123456，5分钟内有效，请勿泄露给他人。
```

#### 步骤3：使用验证码登录

```bash
curl -X POST "http://localhost:8081/user/auth/sms-login" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "13800138000",
    "code": "123456"
  }'
```

**响应**：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTYiLCJwaG9uZSI6IjEzODAwMTM4MDAwIiwiaWF0IjoxNjk5MDA4MDAwLCJleHAiOjE2OTk2MTI4MDB9.xxx",
    "tokenExpiresAt": "2025-11-10T12:00:00",
    "userInfo": {
      "userId": 123456,
      "phone": "13800138000",
      "username": "测试用户"
    }
  }
}
```

### 强制发送验证码示例

**场景**：用户点击"重新发送"时，即使验证码未过期也要发送新的

```bash
curl -X GET "http://localhost:8081/user/msm/send/13800138000?force=true" \
  -H "Accept: application/json"
```

### JavaScript/TypeScript 示例

```typescript
// 发送验证码
async function sendSmsCode(phone: string, force: boolean = false): Promise<boolean> {
  try {
    const response = await fetch(
      `http://localhost:8081/user/msm/send/${phone}?force=${force}`,
      {
        method: 'GET',
        headers: {
          'Accept': 'application/json'
        }
      }
    );
    
    const result = await response.json();
    
    if (result.code === 200) {
      console.log('验证码发送成功');
      return true;
    } else {
      console.error('验证码发送失败:', result.message);
      return false;
    }
  } catch (error) {
    console.error('网络错误:', error);
    return false;
  }
}

// 短信登录
async function smsLogin(phone: string, code: string): Promise<LoginResponse> {
  const response = await fetch('http://localhost:8081/user/auth/sms-login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ phone, code })
  });
  
  const result = await response.json();
  
  if (result.code === 200) {
    // 保存token到localStorage
    localStorage.setItem('token', result.data.token);
    return result.data;
  } else {
    throw new Error(result.message);
  }
}

// 使用示例
async function handleLogin() {
  const phone = '13800138000';
  const code = '123456';
  
  try {
    const loginData = await smsLogin(phone, code);
    console.log('登录成功:', loginData);
    // 跳转到主页
    window.location.href = '/home';
  } catch (error) {
    console.error('登录失败:', error.message);
    alert(error.message);
  }
}
```

---

## ❓ 常见问题

### Q1: 验证码收不到怎么办？

**A**: 检查以下几点：
1. 手机号格式是否正确（11位数字）
2. 查看服务器日志，确认是否发送成功
3. 检查阿里云短信服务余额是否充足
4. 确认短信签名和模板是否已审核通过
5. 检查手机是否有信号，是否被拦截为垃圾短信

### Q2: 提示"验证码已存在"怎么办？

**A**: 两种解决方法：
1. 等待5分钟后重试（验证码自动过期）
2. 使用`force=true`参数强制发送新验证码

```bash
curl -X GET "http://localhost:8081/user/msm/send/13800138000?force=true"
```

### Q3: 验证码输入错误后无法重试？

**A**: 这是安全机制设计，验证码输入错误后会立即失效。需要重新获取新的验证码。

### Q4: 如何查看短信发送记录？

**A**: 查看服务器日志：

```bash
# 查看所有短信相关日志
sudo journalctl -u spring_1818_user_server | grep "短信"

# 查看特定手机号的发送记录
sudo journalctl -u spring_1818_user_server | grep "phone: 13800138000"
```

### Q5: 短信费用如何计算？

**A**: 
- 国内短信：约0.045元/条
- 计费由阿里云短信服务收取
- 可在阿里云控制台查看详细账单

### Q6: 如何修改验证码有效期？

**A**: 修改代码中的过期时间：

```java
// 当前是5分钟
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

// 修改为3分钟
redisTemplate.opsForValue().set(phone, code, 3, TimeUnit.MINUTES);
```

### Q7: 如何修改验证码位数？

**A**: 修改生成验证码的代码：

```java
// 当前是6位（100000-999999）
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));

// 修改为4位（1000-9999）
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
```

### Q8: 如何添加发送频率限制？

**A**: 需要在代码中添加额外的Redis计数器：

```java
// 伪代码示例
String countKey = "sms:count:" + phone;
Integer count = redisTemplate.opsForValue().get(countKey);

if (count != null && count >= 5) {
    return Result.error(429, "发送次数过多，请明天再试");
}

// 发送成功后增加计数
redisTemplate.opsForValue().increment(countKey);
// 设置24小时过期
redisTemplate.expire(countKey, 24, TimeUnit.HOURS);
```

---

## 🛠️ 维护指南

### 监控指标

#### 1. 短信发送成功率

```sql
-- 查看今日短信发送情况（需要添加短信发送记录表）
SELECT 
    DATE(create_time) as date,
    COUNT(*) as total,
    SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
    SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
    ROUND(SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
FROM sms_log
WHERE DATE(create_time) = CURDATE()
GROUP BY DATE(create_time);
```

#### 2. Redis验证码监控

```bash
# 连接Redis
redis-cli

# 查看所有手机号的验证码（生产环境不建议执行）
KEYS *

# 查看特定手机号的验证码
GET 13800138000

# 查看验证码剩余过期时间（秒）
TTL 13800138000
```

#### 3. 日志监控

```bash
# 统计今日短信发送次数
sudo journalctl -u spring_1818_user_server --since today | grep "短信验证码发送成功" | wc -l

# 统计今日短信发送失败次数
sudo journalctl -u spring_1818_user_server --since today | grep "短信验证码发送失败" | wc -l

# 查看最近的错误
sudo journalctl -u spring_1818_user_server -n 100 | grep -E "ERROR.*短信"
```

### 定期检查项

#### 每日检查

- [ ] 查看短信发送成功率
- [ ] 检查阿里云短信服务余额
- [ ] 查看错误日志

#### 每周检查

- [ ] 分析短信发送量趋势
- [ ] 检查异常手机号（发送失败率高的）
- [ ] 审查Redis使用情况

#### 每月检查

- [ ] 审查短信费用
- [ ] 更新AccessKey（建议定期轮换）
- [ ] 检查短信签名和模板有效期

### 故障处理

#### 故障1：大量短信发送失败

**排查步骤**：
1. 检查阿里云短信服务是否正常
2. 检查网络连接是否正常
3. 检查AccessKey是否过期
4. 检查账户余额是否充足
5. 查看详细错误日志

#### 故障2：Redis连接失败

**排查步骤**：
1. 检查Redis服务是否运行
2. 检查Redis连接配置
3. 检查防火墙设置
4. 重启Redis服务

#### 故障3：验证码无法验证

**排查步骤**：
1. 检查Redis中是否存在验证码
2. 检查验证码是否已过期
3. 检查代码逻辑是否正确
4. 查看详细日志

### 性能优化建议

#### 1. Redis连接池优化

```yaml
spring:
  redis:
    lettuce:
      pool:
        max-active: 20  # 最大连接数
        max-idle: 10    # 最大空闲连接数
        min-idle: 5     # 最小空闲连接数
```

#### 2. 短信发送异步化

```java
// 使用@Async异步发送短信
@Async
public CompletableFuture<Boolean> sendAsync(Map<String, Object> param, String phone) {
    boolean result = send(param, phone);
    return CompletableFuture.completedFuture(result);
}
```

#### 3. 添加缓存预热

```java
// 应用启动时检查Redis连接
@PostConstruct
public void init() {
    try {
        redisTemplate.opsForValue().set("health_check", "ok", 10, TimeUnit.SECONDS);
        log.info("Redis连接正常");
    } catch (Exception e) {
        log.error("Redis连接失败", e);
    }
}
```

---

## 📝 更新日志

### v1.0.0 (2025-11-03)

**初始版本**：
- ✅ 实现基础短信验证码发送功能
- ✅ 集成阿里云短信服务
- ✅ 实现Redis验证码存储
- ✅ 实现5分钟过期机制
- ✅ 实现一次性使用机制
- ✅ 添加强制发送模式
- ✅ 完善日志记录

---

## 📞 技术支持

**遇到问题？**

1. 查看本文档的[常见问题](#常见问题)章节
2. 查看服务器日志获取详细错误信息
3. 检查阿里云短信服务控制台
4. 联系技术团队

**相关文档**：
- [阿里云短信服务文档](https://help.aliyun.com/document_detail/101414.html)
- [Redis官方文档](https://redis.io/documentation)
- [Spring Boot Redis文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.nosql.redis)

---

**文档版本**: v1.0.0  
**最后更新**: 2025-11-03  
**维护团队**: 1818AI技术团队

