跳转至

登录功能完整设计 - 最终版

核心设计决策

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绑定(不同浏览器独立)

边界情况测试

  • 验证码过期后重新登录
  • 多次刷新验证码后登录
  • 滑块验证通过后修改密码重试
  • 短信验证码过期后重新获取
  • 网络中断后重新登录