18 KiB
18 KiB
OAuth2.0 认证协议集成开发指南
📋 目录
1. 术语定义
🔍 核心概念
| 术语 | 全称 | 说明 |
|---|---|---|
| SP | Service Provider | 业务系统,如OA系统、订单系统 |
| IDaaS | Identity as a Service | 提供统一身份服务的认证系统平台,即IDP |
2. 业务场景说明
📱 应用场景
业务系统作为SP,需要集成IDaaS的单点登录和单点登出功能。
🎯 核心目标
- 单点登录:用户通过IDaaS认证后,可以访问所有授权的应用
- 单点登出:用户在任意应用登出后,所有关联应用都会登出
- 用户信息同步:获取用户在IDaaS平台的身份信息
💡 实现方式
- 门户集成:用户通过IDaaS门户选择应用进行登录
- 独立登录:业务系统提供独立登录页面,调用IDaaS接口
- API直接调用:通过AK/SK方式直接调用IDaaS登录接口
3. 集成流程概览
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第三方应用
📝 配置步骤
- 使用管理员登录IDaaS平台
- 创建新应用,选择标准协议 → OAuth2模式
- 配置应用基本信息
⚙️ 关键配置项
| 配置项 | 说明 | 示例 |
|---|---|---|
| Redirect URI | 授权码模式下,接收IDaaS返回code的回调地址 | http://oa.com/callback |
| Grant Type | 授权类型,固定选择 | authorization_code |
| Client ID | 应用唯一标识 | 1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U |
| Client Secret | 应用密钥 | vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG |
4.2 对接IDaaS登录
🚀 登录方式选择
方式一:使用IDaaS统一登录页
适用场景:不需要自定义登录页面样式的应用
流程说明:
- 构建授权URL,引导用户跳转到IDaaS登录页
- 用户完成登录后,IDaaS回调业务系统
- 业务系统获取code,换取access_token
- 使用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示例
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'
✅ 成功响应
{
"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) |
📤 请求示例
# 方式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
✅ 成功响应
{
"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 | ❌ | 用户的访问令牌 |
📤 请求示例
# 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相关错误
// 客户端认证失败
{
"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等信息进行设备类型判断,实现不同终端的差异化跳转。
🔐 安全建议
- HTTPS传输: 生产环境务必使用HTTPS协议
- State参数: 使用随机且不可预测的state值防止CSRF攻击
- Token保护: 妥善保存client_secret和access_token
- 回调验证: 验证回调请求的来源和参数完整性
- Token过期: 及时处理token过期和刷新逻辑
8. 示例代码
🐍 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集成示例
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平台版本而有所差异,请以实际平台配置为准。