# 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://10.79.112.85/oauth/authorize?response_type=code&scope=read&client_id=54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO&redirect_uri=http%3a%2f%2f10.79.97.17%2f&state=10ff0be64971c07f893afc332877f68arS8FH2iyZni #### 🔗 请求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平台版本而有所差异,请以实际平台配置为准。