274 lines
8.5 KiB
Markdown
274 lines
8.5 KiB
Markdown
# OAuth2.0 登录数据流程说明
|
|
|
|
## 📋 概述
|
|
|
|
本文档详细说明了中国烟草AI合同及卷宗审核系统中OAuth2.0登录的完整数据流程,从用户点击登录按钮到最终完成身份认证的全过程。
|
|
|
|
## 🔄 完整流程图
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User as 用户浏览器
|
|
participant LoginPage as 登录页面(/login)
|
|
participant IDaaS as IDaaS认证服务器
|
|
participant CallbackPage as 回调页面(/callback)
|
|
participant Session as 会话管理
|
|
participant MainApp as 主应用
|
|
|
|
Note over User,MainApp: 1. 用户发起登录
|
|
User->>LoginPage: 访问登录页面
|
|
LoginPage->>User: 显示登录界面
|
|
User->>LoginPage: 点击"统一身份认证登录"
|
|
|
|
Note over User,MainApp: 2. 生成授权URL并跳转
|
|
LoginPage->>LoginPage: generateState() 生成随机状态值
|
|
LoginPage->>LoginPage: localStorage保存oauth_state
|
|
LoginPage->>LoginPage: 构建授权URL
|
|
LoginPage->>User: window.location.href = authorizeUrl
|
|
|
|
Note over User,MainApp: 3. IDaaS认证流程
|
|
User->>IDaaS: 跳转到IDaaS登录页面
|
|
IDaaS->>User: 显示统一登录界面
|
|
User->>IDaaS: 输入用户名密码完成认证
|
|
IDaaS->>User: 重定向回应用 (带code和state)
|
|
|
|
Note over User,MainApp: 4. 授权码处理
|
|
User->>CallbackPage: 访问/callback?code=xxx&state=yyy
|
|
CallbackPage->>CallbackPage: 验证state参数
|
|
CallbackPage->>IDaaS: POST /oauth/token (用code换token)
|
|
IDaaS-->>CallbackPage: 返回access_token等信息
|
|
|
|
Note over User,MainApp: 5. 获取用户信息
|
|
CallbackPage->>IDaaS: GET /userinfo (带access_token)
|
|
IDaaS-->>CallbackPage: 返回用户详细信息
|
|
|
|
Note over User,MainApp: 6. 创建本地会话
|
|
CallbackPage->>Session: 创建用户会话
|
|
Session->>Session: 保存token、用户信息等
|
|
CallbackPage->>User: 重定向到主应用页面
|
|
User->>MainApp: 成功访问应用
|
|
```
|
|
|
|
## 🔍 详细步骤分析
|
|
|
|
### 步骤1: 用户访问登录页面
|
|
**文件**: `app/routes/login.tsx`
|
|
|
|
- 用户访问需要认证的页面时,系统检测到未登录状态
|
|
- 重定向到 `/login` 页面
|
|
- 登录页面加载时会检查OAuth配置是否完整
|
|
|
|
### 步骤2: 点击登录按钮
|
|
**触发函数**: `handleOAuthLogin()`
|
|
|
|
```typescript
|
|
// 关键操作流程
|
|
1. 创建 OAuthClient 实例
|
|
2. 调用 generateState() 生成随机状态值 (格式: randomString_idp)
|
|
3. 将状态值保存到 localStorage
|
|
4. 调用 getAuthorizeUrl(state) 构建授权URL
|
|
5. 通过 window.location.href 跳转到IDaaS
|
|
```
|
|
|
|
**构建的授权URL格式**:
|
|
```
|
|
http://idaas-server/oauth/authorize?
|
|
response_type=code&
|
|
scope=read&
|
|
client_id=YOUR_CLIENT_ID&
|
|
redirect_uri=http://your-app/callback&
|
|
state=randomString_idp
|
|
```
|
|
|
|
### 步骤3: IDaaS认证服务器处理
|
|
**外部系统**: IDaaS平台
|
|
|
|
- 用户在IDaaS页面输入账号密码
|
|
- IDaaS验证用户身份
|
|
- 认证成功后生成授权码(code)
|
|
- 重定向回应用的回调地址,携带code和state参数
|
|
|
|
### 步骤4: 回调页面处理授权码
|
|
**文件**: `app/routes/callback.tsx`
|
|
**函数**: `loader()`
|
|
|
|
#### 4.1 参数验证
|
|
```typescript
|
|
// 检查错误参数
|
|
if (error) {
|
|
return redirect(`/login?error=${error}`);
|
|
}
|
|
|
|
// 检查授权码
|
|
if (!code) {
|
|
return redirect("/login?error=missing_code");
|
|
}
|
|
|
|
// 验证状态值
|
|
if (!state || !state.endsWith("_idp")) {
|
|
return redirect("/login?error=invalid_state");
|
|
}
|
|
```
|
|
|
|
#### 4.2 换取访问令牌
|
|
**调用**: `oauthClient.getAccessToken(code)`
|
|
|
|
```typescript
|
|
// 发送POST请求到IDaaS
|
|
URL: http://idaas-server/oauth/token
|
|
Method: POST
|
|
Content-Type: application/x-www-form-urlencoded
|
|
Body: {
|
|
grant_type: 'authorization_code',
|
|
code: '从回调URL获取的code',
|
|
client_id: 'OAuth应用ID',
|
|
client_secret: 'OAuth应用密钥',
|
|
redirect_uri: '回调地址'
|
|
}
|
|
```
|
|
|
|
**响应数据**:
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciO...",
|
|
"token_type": "bearer",
|
|
"refresh_token": "eyJhbGciOiJIUzI1...",
|
|
"expires_in": 7199,
|
|
"scope": "read",
|
|
"jti": "唯一标识"
|
|
}
|
|
```
|
|
|
|
### 步骤5: 获取用户信息
|
|
**调用**: `oauthClient.getUserInfo(access_token)`
|
|
|
|
```typescript
|
|
// 发送GET请求获取用户信息
|
|
URL: http://idaas-server/api/bff/v1.2/oauth2/userinfo
|
|
Method: GET
|
|
Headers: {
|
|
'Authorization': 'Bearer access_token'
|
|
}
|
|
```
|
|
|
|
**响应数据**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"code": "200",
|
|
"data": {
|
|
"sub": "用户唯一标识",
|
|
"ou_id": "组织ID",
|
|
"nickname": "用户昵称",
|
|
"phone_number": "手机号",
|
|
"ou_name": "组织名称",
|
|
"email": "邮箱",
|
|
"username": "用户名"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 步骤6: 创建本地会话
|
|
**会话存储**: Remix的Cookie Session Storage
|
|
|
|
```typescript
|
|
// 保存到会话中的信息
|
|
session.set("isAuthenticated", true); // 认证状态
|
|
session.set("accessToken", tokenResponse.access_token); // 访问令牌
|
|
session.set("refreshToken", tokenResponse.refresh_token); // 刷新令牌
|
|
session.set("tokenIssuedAt", Date.now()); // 令牌颁发时间
|
|
session.set("tokenExpiresIn", tokenResponse.expires_in); // 令牌有效期
|
|
session.set("userInfo", userInfo.data); // 用户信息
|
|
session.set("userRole", userRole); // 用户角色
|
|
```
|
|
|
|
### 步骤7: 重定向到目标页面
|
|
- 获取原始请求的重定向URL (如果有)
|
|
- 设置会话Cookie
|
|
- 重定向到目标页面或首页
|
|
|
|
## 🔧 关键技术细节
|
|
|
|
### 状态值(State)安全机制
|
|
- **生成**: 使用随机字符串 + "_idp" 后缀
|
|
- **存储**: 保存到浏览器localStorage
|
|
- **验证**: 回调时检查state参数是否以"_idp"结尾
|
|
- **作用**: 防止CSRF攻击
|
|
|
|
### Token管理策略
|
|
- **存储位置**: 服务器端Session (Cookie)
|
|
- **安全性**: HttpOnly Cookie,防止XSS攻击
|
|
- **过期设置**: Session过期时间与OAuth Token同步 (2小时)
|
|
- **自动刷新**: 提前5分钟自动刷新Token,用户无感知
|
|
- **刷新机制**: 使用refresh_token实现令牌自动续期
|
|
- **容错处理**: 刷新失败时自动清理session,重定向登录
|
|
|
|
### 错误处理机制
|
|
- **参数缺失**: missing_code, invalid_state
|
|
- **网络错误**: token_error, userinfo_error
|
|
- **处理失败**: callback_error
|
|
- **用户友好**: 所有错误都转换为中文提示
|
|
|
|
### 用户角色判断
|
|
```typescript
|
|
// 根据用户名判断角色 (可自定义业务逻辑)
|
|
const userRole = userInfo.data.username === "admin" ? "developer" : "common";
|
|
```
|
|
|
|
## 🚀 后续操作说明
|
|
|
|
### 访问受保护资源
|
|
用户登录成功后,后续的页面访问都会:
|
|
1. 从会话中检查 `isAuthenticated` 状态
|
|
2. **自动检测Token状态**: 验证访问令牌是否过期或即将过期
|
|
3. **智能刷新机制**: 如果Token将在5分钟内过期,自动使用refresh_token刷新
|
|
4. **无感知更新**: Token刷新过程对用户完全透明,会话自动延长
|
|
5. **权限控制**: 根据用户角色控制页面访问权限
|
|
|
|
### Token自动刷新流程
|
|
```mermaid
|
|
graph TD
|
|
A[用户访问页面] --> B[检查Session中的Token]
|
|
B --> C{Token是否存在?}
|
|
C -->|否| D[重定向到登录页]
|
|
C -->|是| E[检查Token状态]
|
|
E --> F{Token即将过期?}
|
|
F -->|否| G[正常访问页面]
|
|
F -->|是| H[使用refresh_token刷新]
|
|
H --> I{刷新成功?}
|
|
I -->|是| J[更新Session中的Token]
|
|
I -->|否| K[清理Session并重定向登录]
|
|
J --> G
|
|
```
|
|
|
|
### 单点登出
|
|
当用户登出时,系统会:
|
|
1. 调用IDaaS的登出接口
|
|
2. 清理本地会话数据
|
|
3. 重定向到登录页面
|
|
|
|
## ⚡ 性能优化建议
|
|
|
|
### ✅ 已实现的优化
|
|
1. **智能令牌刷新**: ✅ 提前5分钟自动刷新,避免用户感知中断
|
|
2. **Session同步**: ✅ Session过期时间与Token同步 (2小时)
|
|
3. **统一Token管理**: ✅ 使用TokenManager统一处理令牌逻辑
|
|
4. **错误容错**: ✅ 刷新失败时优雅降级到重新登录
|
|
|
|
### 🔄 可进一步优化
|
|
1. **缓存策略**: 用户信息可在会话期间缓存,减少重复请求
|
|
2. **错误重试**: 网络请求失败时实现重试机制
|
|
3. **状态持久化**: 考虑将部分状态信息持久化到数据库
|
|
4. **Token预刷新**: 可以在页面加载时检查Token状态并预刷新
|
|
|
|
## 🔒 安全注意事项
|
|
|
|
1. **HTTPS传输**: 生产环境必须使用HTTPS
|
|
2. **密钥保护**: client_secret必须妥善保存在服务器端
|
|
3. **状态验证**: 严格验证state参数防止CSRF
|
|
4. **令牌保护**: 访问令牌不应暴露给前端JavaScript
|
|
5. **会话安全**: 使用安全的Cookie配置
|
|
|
|
---
|
|
|
|
**总结**: 整个OAuth2.0登录流程遵循标准的授权码模式,通过多步验证确保用户身份的安全性,同时提供良好的用户体验和错误处理机制。 |