Files
leaudit-platform-frontend/docs/OAuth2.0认证协议集成指南.md

18 KiB
Raw Permalink Blame History

OAuth2.0 认证协议集成开发指南

📋 目录

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. 集成流程概览

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示例

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}
  • 方法: GETPOST(推荐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等信息进行设备类型判断,实现不同终端的差异化跳转。

🔐 安全建议

  1. HTTPS传输: 生产环境务必使用HTTPS协议
  2. State参数: 使用随机且不可预测的state值防止CSRF攻击
  3. Token保护: 妥善保存client_secret和access_token
  4. 回调验证: 验证回调请求的来源和参数完整性
  5. 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平台版本而有所差异,请以实际平台配置为准。