Files
1818web-hoduan/SMS_VERIFICATION_GUIDE.md
2025-11-14 17:41:15 +08:00

903 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 短信验证系统使用指南
**版本:** 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. 发送成功
├─ 是 → 存入Redis5分钟过期→ 返回成功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
// 存储到Redis5分钟后自动过期
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用户收到短信
```
【星洋智慧】您的验证码为1234565分钟内有效请勿泄露给他人。
```
#### 步骤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. 查看详细错误日志
#### 故障2Redis连接失败
**排查步骤**
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技术团队