# PACS Online 登录流程说明文档 ## 目录 - [1. 概述](#1-概述) - [2. 登录方式](#2-登录方式) - [2.1 用户名密码登录](#21-用户名密码登录) - [2.2 手机号验证码登录](#22-手机号验证码登录) - [2.3 短信二次验证登录](#23-短信二次验证登录) - [3. 接口详细说明](#3-接口详细说明) - [4. 数据流转流程](#4-数据流转流程) - [5. 安全机制](#5-安全机制) - [6. 常见问题](#6-常见问题) --- ## 1. 概述 PACS Online 系统支持三种登录方式: 1. **用户名密码登录**:传统的用户名+密码认证方式 2. **手机号验证码登录**:通过手机号+短信验证码进行认证 3. **短信二次验证登录**:在用户名密码验证通过后,需要额外的短信验证码进行二次验证 所有登录成功后都会返回 JWT Token,用于后续接口的身份认证。 --- ## 2. 登录方式 ### 2.1 用户名密码登录 #### 适用场景 适用于所有普通用户的基础登录方式。 #### 接口信息 - **接口地址**:`POST /auth/login` - **请求参数**: ```json { "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:需要短信二次验证** ```json { "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` - **响应示例**: ```json { "code": 200, "message": "验证码发送成功" } ``` **步骤2:验证码登录** - **接口地址**:`POST /auth/loginByPhone` - **请求参数**: ```json { "phone": "13800138000", "code": "123456" } ``` - **响应示例**: ```json { "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": {用户信息对象} } } ``` 2. **需要短信二次验证**: ```json { "code": 200, "message": "需要短信验证", "data": { "requireSmsVerify": true, "preAuthToken": "预认证凭证", "phone": "138****8000", "userId": 1 } } ``` --- ### 3.2 发送短信验证码(普通登录) **接口地址**:`POST /auth/sendCode` **请求参数**: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | phone | String | 是 | 手机号码 | **响应示例**: ```json { "code": 200, "message": "验证码发送成功" } ``` --- ### 3.3 手机号验证码登录 **接口地址**:`POST /auth/loginByPhone` **请求参数**: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | phone | String | 是 | 手机号码 | | code | String | 是 | 验证码 | **响应示例**: ```json { "code": 200, "message": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "userInfo": { "id": 1, "username": "user001", "phone": "13800138000", ... } } } ``` --- ### 3.4 发送登录验证码(二次验证) **接口地址**:`POST /auth/sendLoginCode` **请求参数**: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | preAuthToken | String | 是 | 预认证凭证(从/auth/login获取) | **响应示例**: ```json { "code": 200, "message": "验证码发送成功", "data": { "phone": "138****8000" } } ``` **错误响应**: ```json { "code": 400, "message": "预认证已过期,请重新登录" } ``` --- ### 3.5 验证码校验并完成登录(二次验证) **接口地址**:`POST /auth/verifySmsLogin` **请求参数**: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | preAuthToken | String | 是 | 预认证凭证 | | code | String | 是 | 短信验证码 | **响应示例**: ```json { "code": 200, "message": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "userInfo": { "id": 1, "username": "doctor001", ... } } } ``` **错误响应**: ```json { "code": 400, "message": "验证码错误或已过期" } ``` --- ### 3.6 退出登录 **接口地址**:`POST /auth/logout` **请求头**: ``` Authorization: Bearer {token} ``` **响应示例**: ```json { "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`:需要短信二次验证 - `0` 或 `NULL`:不需要短信二次验证 ### 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) ```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) ```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) ```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) ```javascript // 为所有请求自动添加 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`): ```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.yml` 或 `JwtUtil.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 | --- ## 联系方式 如有问题或建议,请联系开发团队。