576 lines
18 KiB
Markdown
576 lines
18 KiB
Markdown
# OAuth2.0 认证协议集成开发指南
|
||
|
||
## 📋 目录
|
||
- [1. 术语定义](#1-术语定义)
|
||
- [2. 业务场景说明](#2-业务场景说明)
|
||
- [3. 集成流程概览](#3-集成流程概览)
|
||
- [4. 详细集成步骤](#4-详细集成步骤)
|
||
- [5. API接口详解](#5-api接口详解)
|
||
- [6. 错误处理](#6-错误处理)
|
||
- [7. 注意事项](#7-注意事项)
|
||
- [8. 示例代码](#8-示例代码)
|
||
|
||
## 1. 术语定义
|
||
|
||
### 🔍 核心概念
|
||
| 术语 | 全称 | 说明 |
|
||
|------|------|------|
|
||
| **SP** | Service Provider | 业务系统,如OA系统、订单系统 |
|
||
| **IDaaS** | Identity as a Service | 提供统一身份服务的认证系统平台,即IDP |
|
||
|
||
## 2. 业务场景说明
|
||
|
||
### 📱 应用场景
|
||
业务系统作为SP,需要集成IDaaS的单点登录和单点登出功能。
|
||
|
||
### 🎯 核心目标
|
||
- **单点登录**:用户通过IDaaS认证后,可以访问所有授权的应用
|
||
- **单点登出**:用户在任意应用登出后,所有关联应用都会登出
|
||
- **用户信息同步**:获取用户在IDaaS平台的身份信息
|
||
|
||
### 💡 实现方式
|
||
1. **门户集成**:用户通过IDaaS门户选择应用进行登录
|
||
2. **独立登录**:业务系统提供独立登录页面,调用IDaaS接口
|
||
3. **API直接调用**:通过AK/SK方式直接调用IDaaS登录接口
|
||
|
||
## 3. 集成流程概览
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 用户
|
||
participant SP as 业务系统(SP)
|
||
participant IDaaS as IDaaS平台
|
||
|
||
Note over User,IDaaS: 1. 配置OAuth2应用
|
||
SP->>IDaaS: 在IDaaS平台创建OAuth2应用
|
||
IDaaS-->>SP: 返回client_id和client_secret
|
||
|
||
Note over User,IDaaS: 2. 用户登录流程
|
||
User->>SP: 访问业务系统
|
||
SP->>User: 重定向到IDaaS登录页
|
||
User->>IDaaS: 在IDaaS完成登录
|
||
IDaaS->>SP: 返回authorization code
|
||
SP->>IDaaS: 使用code获取access_token
|
||
IDaaS-->>SP: 返回access_token
|
||
SP->>IDaaS: 使用access_token获取用户信息
|
||
IDaaS-->>SP: 返回用户详细信息
|
||
SP-->>User: 完成登录,访问业务系统
|
||
|
||
Note over User,IDaaS: 3. 用户登出流程
|
||
User->>SP: 请求登出
|
||
SP->>IDaaS: 调用IDaaS登出接口
|
||
IDaaS-->>SP: 登出成功
|
||
SP-->>User: 重定向到登录页
|
||
```
|
||
|
||
## 4. 详细集成步骤
|
||
|
||
### 4.1 配置OAuth2第三方应用
|
||
|
||
#### 📝 配置步骤
|
||
1. 使用管理员登录IDaaS平台
|
||
2. 创建新应用,选择标准协议 → OAuth2模式
|
||
3. 配置应用基本信息
|
||
|
||
#### ⚙️ 关键配置项
|
||
| 配置项 | 说明 | 示例 |
|
||
|--------|------|------|
|
||
| **Redirect URI** | 授权码模式下,接收IDaaS返回code的回调地址 | `http://oa.com/callback` |
|
||
| **Grant Type** | 授权类型,固定选择 | `authorization_code` |
|
||
| **Client ID** | 应用唯一标识 | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` |
|
||
| **Client Secret** | 应用密钥 | `vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG` |
|
||
|
||
### 4.2 对接IDaaS登录
|
||
|
||
#### 🚀 登录方式选择
|
||
|
||
##### 方式一:使用IDaaS统一登录页
|
||
**适用场景**:不需要自定义登录页面样式的应用
|
||
|
||
**流程说明**:
|
||
1. 构建授权URL,引导用户跳转到IDaaS登录页
|
||
2. 用户完成登录后,IDaaS回调业务系统
|
||
3. 业务系统获取code,换取access_token
|
||
4. 使用access_token获取用户信息
|
||
|
||
## 5. API接口详解
|
||
|
||
### 5.1 获取授权码(Authorization Code)
|
||
|
||
#### 📌 接口描述
|
||
引导用户到IDaaS登录页面,获取授权码
|
||
|
||
http://<u>10.79.112.85</u>/oauth/authorize?response_type=code&scope=read&client_id=<u>54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO</u>&redirect_uri=<u>http%3a%2f%2f10.79.97.17%2f</u>&state=<u>10ff0be64971c07f893afc332877f68arS8FH2iyZni</u>
|
||
|
||
#### 🔗 请求URL格式
|
||
```
|
||
http(s)://{IDaaS_server}/oauth/authorize?response_type=code&scope=read&client_id={client_id}&redirect_uri={redirect_uri}&state={state}
|
||
```
|
||
|
||
#### 📋 请求参数
|
||
| 参数名 | 类型 | 必填 | 示例值 | 说明 |
|
||
|--------|------|------|--------|------|
|
||
| `response_type` | string | ✅ | `code` | 响应类型,固定为code |
|
||
| `scope` | string | ✅ | `read` | 授权范围,固定为read |
|
||
| `client_id` | string | ✅ | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` | OAuth2应用的Client ID |
|
||
| `redirect_uri` | string | ✅ | `http%3A%2F%2Foa.com%2Fcallback` | 回调地址(需URL编码) |
|
||
| `state` | string | ✅ | `10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp` | 状态值,建议包含`_idp`后缀 |
|
||
|
||
#### 💡 完整示例
|
||
```
|
||
http://idaas.example.com/oauth/authorize?response_type=code&scope=read&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&redirect_uri=http%3A%2F%2Foa.com%2Fcallback&state=10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp
|
||
```
|
||
|
||
### 5.2 获取访问令牌(Access Token)
|
||
|
||
#### 📌 接口描述
|
||
使用授权码换取访问令牌
|
||
|
||
#### 🔗 请求信息
|
||
- **URL**: `http(s)://{IDaaS_server}/oauth/token`
|
||
- **方法**: `POST`
|
||
- **Content-Type**: `application/x-www-form-urlencoded`
|
||
|
||
#### 📋 请求参数
|
||
| 参数名 | 类型 | 必填 | 示例值 | 说明 |
|
||
|--------|------|------|--------|------|
|
||
| `grant_type` | string | ✅ | `authorization_code` | 授权类型,固定值 |
|
||
| `code` | string | ✅ | `WgWQe6` | 从回调中获取的授权码 |
|
||
| `client_id` | string | ✅ | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` | OAuth2应用的Client ID |
|
||
| `client_secret` | string | ✅ | `vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG` | OAuth2应用的Client Secret |
|
||
| `redirect_uri` | string | ✅ | `http%3A%2F%2Foa.com%2Fcallback` | 回调地址(需URL编码) |
|
||
|
||
#### 📤 cURL示例
|
||
```bash
|
||
curl -X POST 'http://idaas.example.com/oauth/token' \
|
||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||
-d 'grant_type=authorization_code&code=dIKvfA&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&client_secret=vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG&redirect_uri=http%3A%2F%2Foa.com%2Fcallback'
|
||
```
|
||
|
||
#### ✅ 成功响应
|
||
```json
|
||
{
|
||
"access_token": "eyJhbGciO...",
|
||
"token_type": "bearer",
|
||
"refresh_token": "eyJhbGciOiJIUzI1...",
|
||
"expires_in": 7199,
|
||
"scope": "read",
|
||
"jti": "17147278-7f3e-45f2-be6f-8105c4334a30"
|
||
}
|
||
```
|
||
|
||
### 5.3 获取用户信息
|
||
|
||
#### 📌 接口描述
|
||
使用访问令牌获取用户详细信息
|
||
|
||
#### 🔗 请求信息
|
||
- **URL**: `https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo`
|
||
- **方法**: `GET`
|
||
- **认证**: Bearer Token
|
||
|
||
#### 📋 请求参数
|
||
| 参数名 | 类型 | 必填 | 说明 |
|
||
|--------|------|------|------|
|
||
| `access_token` | string | ✅ | 访问令牌(可作为URL参数或Header) |
|
||
|
||
#### 📤 请求示例
|
||
```bash
|
||
# 方式1: URL参数
|
||
GET https://idaas.example.com/api/bff/v1.2/oauth2/userinfo?access_token=eyJhbGc1NiIs...
|
||
|
||
# 方式2: Authorization Header
|
||
curl -H "Authorization: Bearer eyJhbGc1NiIs..." \
|
||
https://idaas.example.com/api/bff/v1.2/oauth2/userinfo
|
||
```
|
||
|
||
#### ✅ 成功响应
|
||
```json
|
||
{
|
||
"success": true,
|
||
"code": "200",
|
||
"message": null,
|
||
"requestId": "149DA248-8F49-4820-B87A-5EA36D932354",
|
||
"data": {
|
||
"sub": "823071756087671783",
|
||
"ou_id": "2079225187122667069",
|
||
"nickname": "测试用户",
|
||
"phone_number": "11136618971",
|
||
"ou_name": "测试组织IDAAS",
|
||
"email": "test@test.com",
|
||
"username": "test"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 📊 响应字段说明
|
||
| 字段名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| `sub` | string | 用户唯一标识 |
|
||
| `ou_id` | string | 组织ID |
|
||
| `nickname` | string | 用户昵称 |
|
||
| `phone_number` | string | 手机号码 |
|
||
| `ou_name` | string | 组织名称 |
|
||
| `email` | string | 邮箱地址 |
|
||
| `username` | string | 用户名 |
|
||
|
||
### 5.4 单点登出(SLO)
|
||
|
||
#### 📌 接口描述
|
||
实现全局统一登出功能
|
||
|
||
#### 🔗 请求信息
|
||
- **URL**: `http(s)://{IDaaS_server}/public/sp/slo/{appId}`
|
||
- **方法**: `GET` 或 `POST`(推荐POST)
|
||
|
||
#### 📋 请求参数
|
||
| 参数名 | 类型 | 必填 | 说明 |
|
||
|--------|------|------|------|
|
||
| `appId` | string | ✅ | 应用ID(路径参数) |
|
||
| `redirect_url` | string | ❌ | 登出成功后的重定向URL(需URL编码) |
|
||
| `access_token` | string | ❌ | 用户的访问令牌 |
|
||
|
||
#### 📤 请求示例
|
||
```bash
|
||
# GET请求
|
||
http://idaas.example.com/public/sp/slo/idaasoauth2?access_token=xxxxxxx&redirect_url=https%3A%2F%2Fwww.example.com%2F
|
||
|
||
# POST请求(推荐)
|
||
curl -X POST 'http://idaas.example.com/public/sp/slo/idaasoauth2' \
|
||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||
-d 'access_token=xxxxxxx&redirect_url=https%3A%2F%2Fwww.example.com%2F'
|
||
```
|
||
|
||
## 6. 错误处理
|
||
|
||
### ❌ 常见错误响应
|
||
|
||
#### Token相关错误
|
||
```json
|
||
// 客户端认证失败
|
||
{
|
||
"error": "invalid_client",
|
||
"error_description": "Bad client credentials"
|
||
}
|
||
|
||
// 授权码无效
|
||
{
|
||
"error": "invalid_grant",
|
||
"error_description": "Invalid authorization code: dIKvfA"
|
||
}
|
||
|
||
// 授权码过期
|
||
{
|
||
"error": "invalid_grant",
|
||
"error_description": "authorization code expired: WgWQe6"
|
||
}
|
||
```
|
||
|
||
#### HTTP状态码说明
|
||
| 状态码 | 错误类型 | 说明 |
|
||
|--------|----------|------|
|
||
| `401` | Unauthorized | 未授权的访问 |
|
||
| `403` | Forbidden | 权限不足 |
|
||
| `404` | ResourceNotFound | 访问的资源不存在 |
|
||
| `415` | UnsupportedMediaType | 不支持的媒体类型 |
|
||
| `500` | InternalError | 服务器内部错误 |
|
||
|
||
## 7. 注意事项
|
||
|
||
### ⚠️ 重要提醒
|
||
|
||
#### 多端访问处理
|
||
当企业内网同时有PC端Web应用和移动端H5应用时,需要根据`remote-user`请求头字段进行判断:
|
||
|
||
- **`remote-user`为NULL**: 从企业内网登录 → 使用原始地址
|
||
- **`remote-user`不为NULL**: 从企业外网登录 → 使用代理地址
|
||
|
||
#### URL地址转换规则
|
||
```
|
||
原始地址: http://xx.YY.zzz.AA
|
||
代理地址: https://xx-YY-zzz-AA-kkkkkkkkkkkk.ztna-dingtalk.com
|
||
```
|
||
|
||
#### 移动端适配
|
||
可以通过UserAgent等信息进行设备类型判断,实现不同终端的差异化跳转。
|
||
|
||
### 🔐 安全建议
|
||
|
||
1. **HTTPS传输**: 生产环境务必使用HTTPS协议
|
||
2. **State参数**: 使用随机且不可预测的state值防止CSRF攻击
|
||
3. **Token保护**: 妥善保存client_secret和access_token
|
||
4. **回调验证**: 验证回调请求的来源和参数完整性
|
||
5. **Token过期**: 及时处理token过期和刷新逻辑
|
||
|
||
## 8. 示例代码
|
||
|
||
### 🐍 Python集成示例
|
||
|
||
```python
|
||
import requests
|
||
import urllib.parse
|
||
from typing import Dict, Optional
|
||
|
||
class IDaaSClient:
|
||
def __init__(self, server_url: str, client_id: str, client_secret: str, redirect_uri: str):
|
||
self.server_url = server_url.rstrip('/')
|
||
self.client_id = client_id
|
||
self.client_secret = client_secret
|
||
self.redirect_uri = redirect_uri
|
||
|
||
def get_authorize_url(self, state: str) -> str:
|
||
"""生成授权URL"""
|
||
params = {
|
||
'response_type': 'code',
|
||
'scope': 'read',
|
||
'client_id': self.client_id,
|
||
'redirect_uri': self.redirect_uri,
|
||
'state': state
|
||
}
|
||
|
||
query_string = urllib.parse.urlencode(params)
|
||
return f"{self.server_url}/oauth/authorize?{query_string}"
|
||
|
||
def get_access_token(self, code: str) -> Optional[Dict]:
|
||
"""使用授权码获取访问令牌"""
|
||
url = f"{self.server_url}/oauth/token"
|
||
data = {
|
||
'grant_type': 'authorization_code',
|
||
'code': code,
|
||
'client_id': self.client_id,
|
||
'client_secret': self.client_secret,
|
||
'redirect_uri': self.redirect_uri
|
||
}
|
||
|
||
try:
|
||
response = requests.post(url, data=data)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except requests.RequestException as e:
|
||
print(f"获取token失败: {e}")
|
||
return None
|
||
|
||
def get_user_info(self, access_token: str) -> Optional[Dict]:
|
||
"""获取用户信息"""
|
||
url = f"{self.server_url}/api/bff/v1.2/oauth2/userinfo"
|
||
headers = {'Authorization': f'Bearer {access_token}'}
|
||
|
||
try:
|
||
response = requests.get(url, headers=headers)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except requests.RequestException as e:
|
||
print(f"获取用户信息失败: {e}")
|
||
return None
|
||
|
||
def logout(self, app_id: str, access_token: str, redirect_url: str) -> bool:
|
||
"""单点登出"""
|
||
url = f"{self.server_url}/public/sp/slo/{app_id}"
|
||
data = {
|
||
'access_token': access_token,
|
||
'redirect_url': redirect_url
|
||
}
|
||
|
||
try:
|
||
response = requests.post(url, data=data)
|
||
return response.status_code == 200
|
||
except requests.RequestException as e:
|
||
print(f"登出失败: {e}")
|
||
return False
|
||
|
||
# 使用示例
|
||
if __name__ == "__main__":
|
||
# 初始化客户端
|
||
client = IDaaSClient(
|
||
server_url="http://idaas.example.com",
|
||
client_id="your_client_id",
|
||
client_secret="your_client_secret",
|
||
redirect_uri="http://your-app.com/callback"
|
||
)
|
||
|
||
# 1. 生成登录URL(重定向用户到此URL)
|
||
state = "random_state_value_with_idp"
|
||
login_url = client.get_authorize_url(state)
|
||
print(f"登录URL: {login_url}")
|
||
|
||
# 2. 处理回调(从query参数获取code)
|
||
code = "received_code_from_callback"
|
||
token_response = client.get_access_token(code)
|
||
|
||
if token_response:
|
||
access_token = token_response['access_token']
|
||
print(f"Access Token: {access_token}")
|
||
|
||
# 3. 获取用户信息
|
||
user_info = client.get_user_info(access_token)
|
||
if user_info and user_info['success']:
|
||
user_data = user_info['data']
|
||
print(f"用户信息: {user_data}")
|
||
|
||
# 4. 登出
|
||
logout_success = client.logout("your_app_id", access_token, "http://your-app.com/login")
|
||
print(f"登出结果: {'成功' if logout_success else '失败'}")
|
||
```
|
||
|
||
### 🌐 JavaScript集成示例
|
||
|
||
```javascript
|
||
class IDaaSClient {
|
||
constructor(serverUrl, clientId, clientSecret, redirectUri) {
|
||
this.serverUrl = serverUrl.replace(/\/$/, '');
|
||
this.clientId = clientId;
|
||
this.clientSecret = clientSecret;
|
||
this.redirectUri = redirectUri;
|
||
}
|
||
|
||
// 生成授权URL
|
||
getAuthorizeUrl(state) {
|
||
const params = new URLSearchParams({
|
||
response_type: 'code',
|
||
scope: 'read',
|
||
client_id: this.clientId,
|
||
redirect_uri: this.redirectUri,
|
||
state: state
|
||
});
|
||
|
||
return `${this.serverUrl}/oauth/authorize?${params.toString()}`;
|
||
}
|
||
|
||
// 获取访问令牌
|
||
async getAccessToken(code) {
|
||
const url = `${this.serverUrl}/oauth/token`;
|
||
const data = new URLSearchParams({
|
||
grant_type: 'authorization_code',
|
||
code: code,
|
||
client_id: this.clientId,
|
||
client_secret: this.clientSecret,
|
||
redirect_uri: this.redirectUri
|
||
});
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: data
|
||
});
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取token失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 获取用户信息
|
||
async getUserInfo(accessToken) {
|
||
const url = `${this.serverUrl}/api/bff/v1.2/oauth2/userinfo`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 单点登出
|
||
async logout(appId, accessToken, redirectUrl) {
|
||
const url = `${this.serverUrl}/public/sp/slo/${appId}`;
|
||
const data = new URLSearchParams({
|
||
access_token: accessToken,
|
||
redirect_url: redirectUrl
|
||
});
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: data
|
||
});
|
||
|
||
return response.ok;
|
||
} catch (error) {
|
||
console.error('登出失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
const client = new IDaaSClient(
|
||
'http://idaas.example.com',
|
||
'your_client_id',
|
||
'your_client_secret',
|
||
'http://your-app.com/callback'
|
||
);
|
||
|
||
// 处理登录流程
|
||
async function handleLogin() {
|
||
// 1. 重定向到IDaaS登录页
|
||
const state = 'random_state_value_with_idp';
|
||
const loginUrl = client.getAuthorizeUrl(state);
|
||
window.location.href = loginUrl;
|
||
}
|
||
|
||
// 处理回调
|
||
async function handleCallback() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const code = urlParams.get('code');
|
||
const state = urlParams.get('state');
|
||
|
||
if (code) {
|
||
// 2. 获取访问令牌
|
||
const tokenResponse = await client.getAccessToken(code);
|
||
|
||
if (tokenResponse && tokenResponse.access_token) {
|
||
const accessToken = tokenResponse.access_token;
|
||
|
||
// 3. 获取用户信息
|
||
const userInfo = await client.getUserInfo(accessToken);
|
||
|
||
if (userInfo && userInfo.success) {
|
||
console.log('用户信息:', userInfo.data);
|
||
// 保存用户信息到localStorage或状态管理
|
||
localStorage.setItem('access_token', accessToken);
|
||
localStorage.setItem('user_info', JSON.stringify(userInfo.data));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理登出
|
||
async function handleLogout() {
|
||
const accessToken = localStorage.getItem('access_token');
|
||
|
||
if (accessToken) {
|
||
const success = await client.logout('your_app_id', accessToken, window.location.origin + '/login');
|
||
|
||
if (success) {
|
||
localStorage.removeItem('access_token');
|
||
localStorage.removeItem('user_info');
|
||
window.location.href = '/login';
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📞 技术支持
|
||
|
||
如需更多技术支持,请参考:
|
||
- IDaaS平台管理后台
|
||
- 相关API文档
|
||
- 集成Demo项目
|
||
|
||
**注意**: 本文档基于OAuth2.0标准协议,具体实现可能因IDaaS平台版本而有所差异,请以实际平台配置为准。 |