登录流程说明.md 26 KB

PACS Online 登录流程说明文档

目录


1. 概述

PACS Online 系统支持三种登录方式:

  1. 用户名密码登录:传统的用户名+密码认证方式
  2. 手机号验证码登录:通过手机号+短信验证码进行认证
  3. 短信二次验证登录:在用户名密码验证通过后,需要额外的短信验证码进行二次验证

所有登录成功后都会返回 JWT Token,用于后续接口的身份认证。


2. 登录方式

2.1 用户名密码登录

适用场景

适用于所有普通用户的基础登录方式。

接口信息

  • 接口地址POST /auth/login
  • 请求参数

    {
    "username": "用户名",
    "password": "密码"
    }
    

    流程说明

    ```

┌──────────┐ │ 前端提交 │ │ 用户名密码│ └────┬─────┘

 │
 ▼

┌────────────────────────────┐ │ 1. 用户名密码认证 │ │ (Spring Security) │ └────┬───────────────────┬───┘

 │                   │
 │ 认证失败          │ 认证成功
 ▼                   ▼

┌─────────┐ ┌──────────────────┐ │返回错误 │ │2. 检查是否需要 │ └─────────┘ │ 短信二次验证 │

             └────┬────────┬─────┘
                  │        │
      需要二次验证│        │不需要二次验证
                  ▼        ▼
        ┌──────────────┐  ┌────────────┐
        │3. 生成并返回 │  │4. 生成JWT  │
        │  preAuthToken│  │   返回token│
        │  和脱敏手机号│  └────────────┘
        └──────────────┘

#### 响应说明

**情况1:不需要二次验证,直接登录成功**
```json
{
  "code": 200,
  "message": "登录成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "userInfo": {
      "id": 1,
      "username": "admin",
      "phone": "13800138000",
      ...
    }
  }
}

情况2:需要短信二次验证

{
  "code": 200,
  "message": "需要短信验证",
  "data": {
    "requireSmsVerify": true,
    "preAuthToken": "abc123def456...",
    "phone": "138****8000",
    "userId": 1
  }
}

判断是否需要二次验证的逻辑

系统根据以下条件判断是否需要短信二次验证:

  1. 用户在 doctors 表中存在记录
  2. 该医生的 is_send_message 字段值为 1
  3. 用户绑定了手机号

满足以上条件时,需要进行短信二次验证。


2.2 手机号验证码登录

适用场景

用户忘记密码或希望快速登录时使用。

流程说明

┌──────────┐
│前端页面  │
└────┬─────┘
     │
     ▼
┌─────────────────────┐
│ 1. 请求发送验证码   │
│    POST /auth/sendCode
│    参数: phone      │
└────┬────────────────┘
     │
     ▼
┌─────────────────────┐
│ 2. 系统生成验证码   │
│    保存到Redis和DB  │
│    有效期5分钟      │
└────┬────────────────┘
     │
     ▼
┌─────────────────────┐
│ 3. 前端提交手机号和 │
│    验证码进行登录   │
│    POST /auth/loginByPhone
└────┬────────────────┘
     │
     ▼
┌─────────────────────┐
│ 4. 验证码校验成功   │
│    生成JWT Token    │
│    返回登录结果     │
└─────────────────────┘

接口详情

步骤1:发送验证码

  • 接口地址POST /auth/sendCode
  • 请求参数phone=13800138000
  • 响应示例

    {
    "code": 200,
    "message": "验证码发送成功"
    }
    

    步骤2:验证码登录

    • 接口地址POST /auth/loginByPhone
    • 请求参数json { "phone": "13800138000", "code": "123456" }
  • 响应示例

    {
    "code": 200,
    "message": "登录成功",
    "data": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "userInfo": {
        "id": 1,
        "username": "user001",
        ...
      }
    }
    }
    

    2.3 短信二次验证登录

    适用场景

    针对需要高安全级别的医生用户,在用户名密码验证通过后,还需要通过手机短信验证码进行二次验证。

    触发条件

    1. 用户在 doctors 表中存在记录
    2. doctors.is_send_message = 1(开启短信二次验证)
    3. 用户已绑定手机号

    完整流程说明

    ```

┌──────────────┐ │1. 用户名密码 │ │ 登录 │ │POST /auth/login └──────┬───────┘

   │
   ▼

┌──────────────────────┐ │2. 认证成功,检查是否│ │ 需要二次验证 │ └──────┬───────────────┘

   │
   ▼

┌──────────────────────┐ │3. 返回preAuthToken │ │ 和脱敏手机号 │ │ requireSmsVerify=true └──────┬───────────────┘

   │
   ▼

┌──────────────────────┐ │4. 前端调用发送 │ │ 验证码接口 │ │POST /auth/sendLoginCode │参数: preAuthToken │ └──────┬───────────────┘

   │
   ▼

┌──────────────────────┐ │5. 系统生成并发送验证码│ │ 保存记录到DB │ │ 返回脱敏手机号 │ └──────┬───────────────┘

   │
   ▼

┌──────────────────────┐ │6. 用户收到验证码 │ │ 前端提交验证 │ │POST /auth/verifySmsLogin │参数: preAuthToken, code └──────┬───────────────┘

   │
   ▼

┌──────────────────────┐ │7. 验证码校验成功 │ │ 生成JWT Token │ │ 删除preAuthToken │ │ 返回登录结果 │ └──────────────────────┘


#### 关键概念

**preAuthToken(预认证凭证)**
- 在用户名密码验证通过后生成
- 存储在 Redis 中,key 为 `preauth:{token}`,value 为 `userId`
- 有效期 5 分钟
- 用于关联后续的短信验证流程

**验证码生成和存储**
- 验证码为 6 位数字
- 同时存储在:
  - Redis:`sms:code:{phone}`,有效期 5 分钟
  - 数据库:`sys_sms_code` 表
  - 数据库:`sys_login_sms_record` 表(登录专用记录)

---

## 3. 接口详细说明

### 3.1 用户名密码登录

**接口地址**:`POST /auth/login`

**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| username | String | 是 | 用户名 |
| password | String | 是 | 密码(明文) |

**响应参数**:

根据不同情况,返回不同的数据结构:

1. **直接登录成功**(不需要二次验证):
   ```json
   {
     "code": 200,
     "message": "登录成功",
     "data": {
       "token": "JWT令牌",
       "userInfo": {用户信息对象}
     }
   }
  1. 需要短信二次验证

    {
     "code": 200,
     "message": "需要短信验证",
     "data": {
       "requireSmsVerify": true,
       "preAuthToken": "预认证凭证",
       "phone": "138****8000",
       "userId": 1
     }
    }
    

    3.2 发送短信验证码(普通登录)

    接口地址POST /auth/sendCode

    请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | phone | String | 是 | 手机号码 |

    响应示例

    {
    "code": 200,
    "message": "验证码发送成功"
    }
    

3.3 手机号验证码登录

接口地址POST /auth/loginByPhone

请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | phone | String | 是 | 手机号码 | | code | String | 是 | 验证码 |

响应示例

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "userInfo": {
      "id": 1,
      "username": "user001",
      "phone": "13800138000",
      ...
    }
  }
}

3.4 发送登录验证码(二次验证)

接口地址POST /auth/sendLoginCode

请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | preAuthToken | String | 是 | 预认证凭证(从/auth/login获取) |

响应示例

{
  "code": 200,
  "message": "验证码发送成功",
  "data": {
    "phone": "138****8000"
  }
}

错误响应

{
  "code": 400,
  "message": "预认证已过期,请重新登录"
}

3.5 验证码校验并完成登录(二次验证)

接口地址POST /auth/verifySmsLogin

请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | preAuthToken | String | 是 | 预认证凭证 | | code | String | 是 | 短信验证码 |

响应示例

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "userInfo": {
      "id": 1,
      "username": "doctor001",
      ...
    }
  }
}

错误响应

{
  "code": 400,
  "message": "验证码错误或已过期"
}

3.6 退出登录

接口地址POST /auth/logout

请求头

Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "退出成功"
}

4. 数据流转流程

4.1 用户名密码登录数据流

┌─────────┐          ┌──────────────┐          ┌──────────┐
│ 前端    │          │ 后端         │          │ 数据存储 │
└────┬────┘          └──────┬───────┘          └────┬─────┘
     │                      │                       │
     │ 1.POST /auth/login   │                       │
     │ {username,password}  │                       │
     ├─────────────────────►│                       │
     │                      │                       │
     │                      │ 2.查询用户            │
     │                      ├──────────────────────►│
     │                      │                  sys_user
     │                      │◄──────────────────────┤
     │                      │                       │
     │                      │ 3.验证密码(BCrypt)    │
     │                      │                       │
     │                      │ 4.查询医生信息        │
     │                      ├──────────────────────►│
     │                      │                  doctors
     │                      │◄──────────────────────┤
     │                      │                       │
     │                      │ 5.判断是否需要二次验证│
     │                      │                       │
     │        ┌─────────────┴─────────────┐         │
     │        │                           │         │
     │        ▼ 不需要                   ▼ 需要    │
     │   6.生成JWT Token            7.生成preAuthToken
     │   存储到Redis                存储到Redis    │
     │   key:token:{userId}         key:preauth:{token}
     │        │                           │         │
     │◄───────┤                           ├────────►│
     │ 返回token                    返回preAuthToken
     │                                    和脱敏手机号
     │                                              │

4.2 短信二次验证数据流

┌─────────┐          ┌──────────────┐          ┌──────────┐
│ 前端    │          │ 后端         │          │ 数据存储 │
└────┬────┘          └──────┬───────┘          └────┬─────┘
     │                      │                       │
     │ 1.POST /auth/sendLoginCode                   │
     │    {preAuthToken}    │                       │
     ├─────────────────────►│                       │
     │                      │                       │
     │                      │ 2.从Redis验证token    │
     │                      ├──────────────────────►│
     │                      │    get preauth:{token}│
     │                      │◄──────────────────────┤
     │                      │    返回userId         │
     │                      │                       │
     │                      │ 3.生成6位验证码       │
     │                      │                       │
     │                      │ 4.保存验证码          │
     │                      ├──────────────────────►│
     │                      │ Redis: sms:code:{phone}
     │                      │ DB: sys_sms_code      │
     │                      │ DB: sys_login_sms_record
     │                      │                       │
     │                      │ 5.发送短信(当前为模拟)│
     │                      │                       │
     │◄─────────────────────┤                       │
     │ 返回脱敏手机号       │                       │
     │                      │                       │
     │ 6.POST /auth/verifySmsLogin                  │
     │    {preAuthToken,code}                       │
     ├─────────────────────►│                       │
     │                      │                       │
     │                      │ 7.验证preAuthToken    │
     │                      ├──────────────────────►│
     │                      │                       │
     │                      │ 8.验证验证码          │
     │                      ├──────────────────────►│
     │                      │ get sms:code:{phone}  │
     │                      │◄──────────────────────┤
     │                      │                       │
     │                      │ 9.验证成功,生成JWT   │
     │                      │   删除preAuthToken    │
     │                      │   更新验证记录状态    │
     │                      ├──────────────────────►│
     │                      │                       │
     │◄─────────────────────┤                       │
     │ 返回JWT Token        │                       │
     │                      │                       │

5. 安全机制

5.1 密码安全

  • 使用 BCrypt 算法加密存储密码
  • 密码强度由 Spring Security 框架验证
  • 支持密码重置功能

5.2 Token 安全

  • JWT Token 采用 HS256 算法签名
  • Token 有效期为 24 小时
  • Token 同时存储在 Redis 中,支持强制过期

5.3 验证码安全

  • 验证码为 6 位随机数字
  • 有效期 5 分钟
  • 一次性使用(验证后立即失效)
  • 同时存储在 Redis 和数据库,双重保障

5.4 预认证凭证安全

  • preAuthToken 使用 UUID 生成,不可预测
  • 有效期 5 分钟
  • 验证成功后立即删除
  • 验证失败或过期会更新数据库记录状态

5.5 接口安全

  • 登录相关接口配置在 Spring Security 白名单中
  • 其他业务接口需要 JWT Token 认证
  • 支持 CORS 跨域配置
  • 采用无状态 Session 策略

5.6 日志审计

  • 所有登录操作记录到 sys_log
  • 短信发送记录保存到 sys_login_sms_record
  • 包含操作时间、IP地址、操作结果等信息

6. 常见问题

6.1 为什么需要短信二次验证?

对于医生等重要角色,涉及患者隐私数据的访问,需要更高的安全级别。短信二次验证可以有效防止密码泄露导致的账户被盗用。

6.2 preAuthToken 的作用是什么?

preAuthToken 是一个临时凭证,用于关联用户名密码验证和短信验证两个步骤。它的存在是为了:

  1. 避免在第一步就生成正式的 JWT Token
  2. 确保只有完成两步验证的用户才能获得正式 Token
  3. 实现验证流程的状态管理

6.3 验证码有效期是多久?

  • 普通登录验证码:5 分钟
  • 二次验证验证码:5 分钟
  • preAuthToken:5 分钟

6.4 验证码可以重复使用吗?

不可以。验证码是一次性的,验证成功后会立即从 Redis 中删除,并在数据库中标记为已使用。

6.5 如何配置是否需要短信二次验证?

doctors 表中设置 is_send_message 字段:

  • 1:需要短信二次验证
  • 0NULL:不需要短信二次验证

6.6 如果用户没有绑定手机号怎么办?

如果需要短信验证的用户没有绑定手机号,系统会返回错误提示:"用户未绑定手机号,无法进行短信验证,请联系管理员"。

6.7 Token 过期后怎么办?

Token 过期后需要重新登录。系统会返回 401 未授权错误,前端应引导用户跳转到登录页面。

6.8 如何移除短信二次验证功能?

如果后续不需要此功能,代码中已添加明确的标记注释,搜索 【短信二次验证功能】 即可找到所有相关代码块,按注释提示删除即可。


7. 数据库表说明

7.1 sys_user(用户表)

存储用户基本信息,包括用户名、密码(加密)、手机号等。

7.2 doctors(医生表)

存储医生信息,其中 is_send_message 字段控制是否启用短信二次验证。

7.3 sys_sms_code(短信验证码表)

存储所有发送的短信验证码记录,包括:

  • 手机号
  • 验证码
  • 创建时间
  • 过期时间
  • 是否已使用
  • 发送状态

7.4 sys_login_sms_record(登录短信记录表)

专门记录短信二次验证的发送和验证记录,包括:

  • 用户ID
  • 用户名
  • 手机号
  • 验证码
  • preAuthToken
  • 发送状态
  • 验证状态
  • IP地址

7.5 sys_log(系统日志表)

记录所有登录操作,包括:

  • 操作用户
  • 操作内容
  • 操作时间
  • IP地址
  • 操作结果

8. 前端集成示例

8.1 普通登录(JavaScript)

// 普通用户名密码登录
async function login(username, password) {
  const response = await fetch('/auth/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  });

  const result = await response.json();

  if (result.code === 200) {
    if (result.data.requireSmsVerify) {
      // 需要短信二次验证
      showSmsVerifyDialog(result.data.preAuthToken, result.data.phone);
    } else {
      // 登录成功,保存token
      localStorage.setItem('token', result.data.token);
      localStorage.setItem('userInfo', JSON.stringify(result.data.userInfo));
      window.location.href = '/dashboard.html';
    }
  } else {
    alert('登录失败:' + result.message);
  }
}

8.2 短信二次验证(JavaScript)

// 发送验证码
async function sendLoginCode(preAuthToken) {
  const response = await fetch('/auth/sendLoginCode?preAuthToken=' + preAuthToken, {
    method: 'POST'
  });

  const result = await response.json();

  if (result.code === 200) {
    alert('验证码已发送至 ' + result.data.phone);
    startCountdown(60); // 开始60秒倒计时
  } else {
    alert('发送失败:' + result.message);
  }
}

// 验证验证码并完成登录
async function verifySmsLogin(preAuthToken, code) {
  const response = await fetch('/auth/verifySmsLogin', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: `preAuthToken=${preAuthToken}&code=${code}`
  });

  const result = await response.json();

  if (result.code === 200) {
    // 登录成功,保存token
    localStorage.setItem('token', result.data.token);
    localStorage.setItem('userInfo', JSON.stringify(result.data.userInfo));
    window.location.href = '/dashboard.html';
  } else {
    alert('验证失败:' + result.message);
  }
}

8.3 手机号验证码登录(JavaScript)

// 发送验证码
async function sendPhoneCode(phone) {
  const response = await fetch('/auth/sendCode?phone=' + phone, {
    method: 'POST'
  });

  const result = await response.json();

  if (result.code === 200) {
    alert('验证码已发送');
    startCountdown(60);
  } else {
    alert('发送失败:' + result.message);
  }
}

// 手机号验证码登录
async function loginByPhone(phone, code) {
  const response = await fetch('/auth/loginByPhone', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ phone, code })
  });

  const result = await response.json();

  if (result.code === 200) {
    localStorage.setItem('token', result.data.token);
    localStorage.setItem('userInfo', JSON.stringify(result.data.userInfo));
    window.location.href = '/dashboard.html';
  } else {
    alert('登录失败:' + result.message);
  }
}

8.4 请求拦截器(添加Token)

// 为所有请求自动添加 Token
fetch = (originalFetch => {
  return (...args) => {
    const token = localStorage.getItem('token');
    if (token && args[1]) {
      args[1].headers = args[1].headers || {};
      args[1].headers['Authorization'] = 'Bearer ' + token;
    }
    return originalFetch(...args).then(response => {
      // 如果返回401,跳转到登录页
      if (response.status === 401) {
        localStorage.removeItem('token');
        localStorage.removeItem('userInfo');
        window.location.href = '/login.html';
      }
      return response;
    });
  };
})(fetch);

9. 配置说明

9.1 Spring Security 配置

登录相关接口已配置在白名单中(位于 SecurityConfig.java):

.antMatchers("/auth/login",
             "/auth/logout",
             "/auth/sendCode",
             "/auth/loginByPhone",
             "/auth/sendLoginCode",
             "/auth/verifySmsLogin").permitAll()

9.2 Redis 配置

需要确保 Redis 服务正常运行,系统使用 Redis 存储:

  • JWT Token:token:{userId},有效期 24 小时
  • 验证码:sms:code:{phone},有效期 5 分钟
  • 预认证凭证:preauth:{token},有效期 5 分钟

9.3 JWT 配置

JWT 相关配置在 application.ymlJwtUtil.java 中,包括:

  • 签名密钥(secret)
  • Token 有效期(默认 24 小时)
  • Token 前缀(Bearer)

10. 附录

10.1 错误码说明

错误码 说明
200 成功
400 参数错误或业务逻辑错误
401 未授权(Token无效或过期)
403 禁止访问(用户被禁用)
404 资源不存在(用户不存在)
500 服务器内部错误

10.2 相关文件清单

Java 类文件:

  • SysUserController.java - 登录相关接口控制器
  • SecurityConfig.java - Spring Security 配置
  • JwtAuthenticationFilter.java - JWT 认证过滤器
  • UserDetailsServiceImpl.java - 用户认证服务
  • SysSmsCodeService.java - 短信验证码服务接口
  • SysSmsCodeServiceImpl.java - 短信验证码服务实现
  • SysLoginSmsRecordService.java - 登录短信记录服务
  • JwtUtil.java - JWT 工具类

数据库表:

  • sys_user - 用户表
  • doctors - 医生表
  • sys_sms_code - 短信验证码表
  • sys_login_sms_record - 登录短信记录表
  • sys_log - 系统日志表

文档更新记录

版本 日期 修改内容 修改人
1.0 2025-11-28 初始版本,包含三种登录方式的完整说明 Claude

联系方式

如有问题或建议,请联系开发团队。