登录功能完整设计 - 最终版¶
核心设计决策¶
1. 失败次数计数标识¶
- 基于:手机号 + IP 地址
- 目的:防止同一IP多账户攻击,同时防止同一账户跨IP攻击
- 实现:后端维护
(Phone, IP)的失败记录
2. 跨设备登录¶
- 失败次数:独立计算
- 说明:同一账户在不同设备/IP上的失败次数分别计算
- 场景:用户在家失败5次被锁定,在公司仍可以登录
3. 验证码绑定¶
- 绑定方式:与 Session 绑定
- 说明:不同浏览器/设备的验证码独立
- 安全性:防止验证码被其他设备使用
4. 图形验证码刷新¶
- 规则:无限刷新
- 旧验证码:刷新时立即失效
- 新验证码:立即生效
- 实现:每次刷新时覆盖 Session 中的验证码
5. 滑块验证码重试¶
- 规则:无限重试
- 同一验证码:可以无限次拖动
- 失败处理:不清除验证码,允许继续拖动
6. 短信验证码重发¶
- 规则:旧验证码未失效时,不允许重发
- 检查逻辑:
- 如果旧验证码还在有效期内 → 禁用"获取验证码"按钮,显示"请先使用已发送的验证码"
- 如果旧验证码已过期 → 允许重新发送
- 有效期:5分钟
- 重发时:旧验证码失效,新验证码生效
7. 登录成功后的清理¶
- 清除内容:
- 密码失败次数
- 短信验证码失败次数
- 所有验证码(图形、滑块、短信)
- 所有登录相关的 Session 数据
- 保留内容:用户信息、SessionId、UserId
完整的登录流程¶
密码登录流程(详细版)¶
Text Only
1. 用户输入手机号和密码
↓
2. 后端检查账户是否被锁定 (基于 Phone+IP)
├─ 是 → 返回错误,显示锁定提示和剩余时间
└─ 否 → 继续
↓
3. 后端检查失败次数 (基于 Phone+IP)
├─ 0次 → 直接尝试登录
└─ ≥1次 → 检查滑块验证码是否已验证
├─ 未验证 → 返回错误,显示验证码
├─ 已过期 → 返回错误,显示验证码
└─ 已验证 → 继续登录
↓
4. 前端显示验证码(如果需要)
├─ 图形验证码
└─ 滑块验证码
↓
5. 用户验证图形验证码
├─ 成功 → captchaValid = true
└─ 失败 → 提示错误,允许重试
↓
6. 用户拖动滑块验证
├─ 成功 → sliderValidated = true,设置 Session
└─ 失败 → 提示错误,允许无限重试
↓
7. 用户输入密码并提交登录
├─ 成功 → 登录成功,清除所有登录数据
└─ 失败 → 失败次数+1 (基于 Phone+IP)
├─ 如果 ≥5 → 账户锁定15分钟,显示锁定提示
└─ 如果 <5 → 保持验证码显示,用户重试
短信登录流程(详细版)¶
Text Only
1. 用户输入手机号
↓
2. 后端检查账户是否被锁定 (基于 Phone+IP)
├─ 是 → 返回错误,显示锁定提示和剩余时间
└─ 否 → 继续
↓
3. 前端显示验证码
├─ 图形验证码
└─ 滑块验证码
↓
4. 用户验证图形验证码
├─ 成功 → captchaValid = true
└─ 失败 → 提示错误,允许重试
↓
5. 用户拖动滑块验证
├─ 成功 → sliderValidated = true,设置 Session
└─ 失败 → 提示错误,允许无限重试
↓
6. 用户点击"获取验证码"按钮
├─ 检查短信发送是否被锁定 (基于 Phone+IP)
│ ├─ 是 → 显示"短信发送过于频繁"提示,禁用按钮
│ └─ 否 → 继续
├─ 检查是否有未失效的短信验证码
│ ├─ 是 → 显示"请先使用已发送的验证码",禁用按钮
│ └─ 否 → 继续
├─ 成功 → 发送短信,开始60秒倒计时
└─ 失败 → 提示错误
↓
7. 用户输入短信验证码并提交
├─ 成功 → 登录成功,清除所有登录数据
└─ 失败 → 短信验证失败次数+1 (基于 Phone+IP)
├─ 如果 ≥5 → 短信发送被锁定15分钟,禁用"获取验证码"按钮
└─ 如果 <5 → 提示错误,用户重试
验证码管理详细规则¶
图形验证码¶
| 属性 | 值 | 说明 |
|---|---|---|
| 显示时机 | 登录失败1次后 | 密码登录和短信登录都需要 |
| 有效期 | 5分钟 | 从生成时开始计时 |
| 验证方式 | 用户输入 | 不区分大小写 |
| 刷新规则 | 无限刷新 | 刷新时旧验证码立即失效 |
| 绑定方式 | Session | 不同浏览器/设备独立 |
| Session键 | CaptchaCode |
存储验证码文本 |
| 时间键 | CaptchaGeneratedTime |
存储生成时间戳 |
滑块验证码¶
| 属性 | 值 | 说明 |
|---|---|---|
| 显示时机 | 登录失败1次后 | 密码登录和短信登录都需要 |
| 有效期 | 5分钟 | 从生成时开始计时 |
| 验证方式 | 拖动到80%位置 | 自动验证 |
| 重试规则 | 无限重试 | 同一验证码可无限次拖动 |
| 验证标记 | 一次性使用 | 验证通过后清除 |
| 绑定方式 | Session | 不同浏览器/设备独立 |
| Session键 | SliderCaptchaValidated |
标记是否已验证 |
| 时间键 | SliderValidatedTime |
存储验证时间戳 |
| Token键 | SliderToken |
存储验证Token |
| 生成时间键 | SliderGeneratedTime |
存储生成时间戳 |
短信验证码¶
| 属性 | 值 | 说明 |
|---|---|---|
| 显示时机 | 用户点击"获取验证码" | 仅短信登录 |
| 有效期 | 5分钟 | 从发送时开始计时 |
| 验证方式 | 用户输入 | 6位数字 |
| 重发规则 | 旧验证码未失效时不允许重发 | 防止验证码泛滥 |
| 重发时处理 | 旧验证码失效,新验证码生效 | 覆盖旧验证码 |
| 绑定方式 | Session | 不同浏览器/设备独立 |
| Session键 | SmsCode |
存储短信验证码 |
| 时间键 | SmsCodeGeneratedTime |
存储生成时间戳 |
失败次数管理详细规则¶
密码登录失败次数¶
| 属性 | 值 | 说明 |
|---|---|---|
| 计数标识 | Phone + IP | 同一账户不同IP独立计算 |
| 触发条件 | 密码错误 | 每次登录失败+1 |
| 锁定条件 | ≥ 5次 | 账户锁定15分钟 |
| 锁定范围 | 整个账户 | 无法进行任何登录尝试 |
| 清除条件 | 登录成功 | 清除失败记录 |
| Session键 | FailedLoginAttempts_{Phone}_{IP} |
存储失败次数 |
| 时间键 | LastFailedLoginTime_{Phone}_{IP} |
存储最后失败时间 |
| 锁定键 | AccountLockTime_{Phone}_{IP} |
存储锁定时间戳 |
短信验证码失败次数¶
| 属性 | 值 | 说明 |
|---|---|---|
| 计数标识 | Phone + IP | 同一账户不同IP独立计算 |
| 触发条件 | 短信验证码错误 | 每次验证失败+1 |
| 锁定条件 | ≥ 5次 | 短信发送被锁定15分钟 |
| 锁定范围 | 仅短信发送 | 无法获取新的短信验证码 |
| 清除条件 | 登录成功 | 清除失败记录 |
| Session键 | SmsVerificationFailures_{Phone}_{IP} |
存储失败次数 |
| 时间键 | LastSmsFailureTime_{Phone}_{IP} |
存储最后失败时间 |
| 锁定键 | SmsLockTime_{Phone}_{IP} |
存储锁定时间戳 |
登录成功后的清理¶
需要清除的数据¶
Text Only
密码登录相关:
- FailedLoginAttempts_{Phone}_{IP}
- LastFailedLoginTime_{Phone}_{IP}
- AccountLockTime_{Phone}_{IP}
短信验证码相关:
- SmsVerificationFailures_{Phone}_{IP}
- LastSmsFailureTime_{Phone}_{IP}
- SmsLockTime_{Phone}_{IP}
验证码相关:
- CaptchaCode
- CaptchaGeneratedTime
- SliderToken
- SliderGeneratedTime
- SliderCaptchaValidated
- SliderValidatedTime
- SmsCode
- SmsCodeGeneratedTime
需要保留的数据¶
Text Only
- UserInfo(用户信息)
- SessionId(会话ID)
- UserId(用户ID)
关键实现细节¶
1. IP地址获取¶
C#
// 获取客户端真实IP(考虑代理)
var ip = HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault()?.Split(',').First().Trim()
?? HttpContext.Request.Headers["X-Real-IP"].FirstOrDefault()
?? HttpContext.Connection.RemoteIpAddress?.ToString()
?? "unknown";
2. 短信验证码重发检查¶
C#
// 检查是否有未失效的短信验证码
var existingSmsCode = HttpContext.Session.GetString("SmsCode");
var smsCodeTime = HttpContext.Session.GetInt32("SmsCodeGeneratedTime");
if (!string.IsNullOrEmpty(existingSmsCode) && smsCodeTime.HasValue)
{
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (currentTime - smsCodeTime.Value < 300) // 5分钟内
{
return Json(new { success = false, message = "请先使用已发送的验证码" });
}
}
3. 登录成功后的清理¶
C#
// 清除所有登录相关的Session数据
var keysToRemove = new[]
{
$"FailedLoginAttempts_{phone}_{ip}",
$"LastFailedLoginTime_{phone}_{ip}",
$"AccountLockTime_{phone}_{ip}",
$"SmsVerificationFailures_{phone}_{ip}",
$"LastSmsFailureTime_{phone}_{ip}",
$"SmsLockTime_{phone}_{ip}",
"CaptchaCode",
"CaptchaGeneratedTime",
"SliderToken",
"SliderGeneratedTime",
"SliderCaptchaValidated",
"SliderValidatedTime",
"SmsCode",
"SmsCodeGeneratedTime"
};
foreach (var key in keysToRemove)
{
HttpContext.Session.Remove(key);
}
4. 前端验证码过期检查¶
图形验证码过期处理: - 前端需要记录图形验证码的生成时间 - 每5分钟自动刷新图形验证码 - 用户提交时检查是否过期
滑块验证码过期处理: - 前端需要记录滑块验证码的生成时间 - 每5分钟自动刷新滑块验证码 - 用户提交时检查是否过期
5. 前端登录按钮提交检查¶
密码登录提交前检查: - [ ] 手机号不为空 - [ ] 密码不为空 - [ ] 如果显示了验证码,必须验证图形验证码(captchaValid = true) - [ ] 如果显示了验证码,必须验证滑块验证码(sliderValidated = true) - [ ] 账户未被锁定
短信登录提交前检查: - [ ] 手机号不为空 - [ ] 短信验证码不为空 - [ ] 必须验证图形验证码(captchaValid = true) - [ ] 必须验证滑块验证码(sliderValidated = true) - [ ] 账户未被锁定
6. 账户被锁定时的前端处理¶
显示内容: - 显示锁定提示和精确剩余时间 - 显示所有输入框(但禁用提交) - 显示倒计时
禁用内容: - 禁用登录提交按钮 - 禁用"获取验证码"按钮(如果是短信登录)
倒计时更新: - 后端返回剩余秒数 - 前端每秒更新一次显示 - 倒计时结束后自动刷新页面或隐藏锁定提示
测试清单¶
密码登录测试¶
- 首次登录成功
- 失败1次后显示验证码
- 失败2-4次显示剩余尝试次数
- 失败5次账户锁定
- 账户锁定期间显示精确剩余时间
- 账户锁定15分钟后自动解锁
- 同一账户不同IP的失败次数独立计算
- 登录成功后清除所有登录数据
短信登录测试¶
- 显示验证码(图形 + 滑块)
- 短信验证码失败1-4次显示剩余尝试次数
- 短信验证码失败5次禁用"获取验证码"按钮
- 短信发送锁定期间显示精确剩余时间
- 短信发送锁定15分钟后自动解锁
- 旧验证码未失效时不允许重发
- 旧验证码失效后允许重发
- 登录成功后清除所有登录数据
验证码测试¶
- 图形验证码可无限刷新
- 刷新时旧验证码立即失效
- 滑块验证码可无限重试
- 验证码过期后提示刷新
- 验证码与Session绑定(不同浏览器独立)
边界情况测试¶
- 验证码过期后重新登录
- 多次刷新验证码后登录
- 滑块验证通过后修改密码重试
- 短信验证码过期后重新获取
- 网络中断后重新登录