金牛ID OAuth 开发文档
金牛ID提供标准的OAuth 2.0授权服务,让第三方应用可以快速接入金牛ID账号体系,实现一键登录功能。
OAuth 2.0 授权流程
金牛ID采用OAuth 2.0标准的授权码模式(Authorization Code Flow),流程如下:
- 第三方应用引导用户访问金牛ID授权页面
- 用户登录并同意授权
- 金牛ID重定向回第三方应用,携带授权码或错误信息
- 第三方应用使用授权码换取访问令牌
- 使用访问令牌获取用户信息
回调错误码说明
当授权过程中出现错误时,金牛ID会将错误信息通过回调地址返回给第三方应用。错误参数如下:
| 错误码 (error) | 说明 | 处理方式 |
|---|---|---|
| unsupported_response_type | 仅支持code响应类型 | 检查response_type参数是否为code |
| invalid_client | 客户端ID无效或不存在 | 检查client_id是否正确,应用是否已注册 |
| invalid_redirect_uri | 回调地址不在白名单中 | 在API密钥管理页面配置回调地址白名单 |
| account_disabled | 用户账号已被禁用 | 提示用户联系客服了解详情或申请解封 |
| realname_required | 用户未完成实名认证 | 引导用户完成实名认证,企业用户需上传营业执照 |
| idcard_expired | 用户身份证已过期 | 引导用户更新身份证信息后重新认证 |
| access_denied | 用户拒绝授权 | 提示用户需要授权才能继续 |
| server_error | 系统错误 | 稍后重试或联系客服 |
错误回调参数说明:
error- 错误码(见上表)error_description- 详细错误信息,包含排查建议error_reason- 错误原因代码(部分错误提供)user_type- 用户类型(enterprise/personal,部分错误提供)state- 原样返回请求中的state参数(如果授权请求中包含)
特殊错误说明
| 错误场景 | error_reason | user_type | 说明 |
|---|---|---|---|
| real_name_required | enterprise_auth_pending | enterprise | 企业用户未完成企业认证(需上传营业执照和法人身份证) |
| real_name_required | personal_auth_pending | personal | 个人用户未完成实名认证 |
| idcard_expired | idcard_expired | enterprise/personal | 身份证已过期,需重新认证 |
接入指南
用户类型说明
金牛ID支持两种用户类型,请根据您的需求选择合适的认证方式:
| 特性 | 个人用户 | 企业用户 |
|---|---|---|
| 认证依据 | 中国大陆居民身份证 | 营业执照(统一社会信用代码) |
| 应用管理 | 不支持 | 支持(最多300个应用) |
| 生成API Key | 不支持 | 支持(每个应用1个令牌) |
| 主要用途 | 个人登录、实名认证 | 第三方应用接入、统一身份认证 |
1. 注册并完成认证
首先,您需要注册一个金牛ID账号并完成实名认证。企业用户完成企业认证后,需要按照以下流程获取 API 密钥:
企业用户获取 API 密钥流程:
- 创建应用 - 进入应用管理页面,创建新应用(最多300个)
- 配置白名单 - 设置回调域名和IP白名单
- 创建令牌 - 为应用创建 API 令牌(每个应用限1个)
- 保存密钥 - 获取 API Key(Client ID)和 API Secret(Client Secret)
提示:详细操作步骤请参考应用管理章节。
2. 配置回调地址白名单
为了安全起见,金牛ID要求所有回调地址必须在白名单中注册。请在API密钥管理页面配置允许的回调地址。
OAuth授权只接受来自白名单中域名或IP的回调请求。如果回调地址不在白名单中,授权将被拒绝。
白名单配置说明
API密钥管理页面提供两个独立的输入框,请分别配置:
| 输入框 | 支持的格式 | 示例 |
|---|---|---|
| 域名白名单 | 域名或通配符域名 | example.com, *.example.com, app.yourdomain.com |
| IP白名单 | IP地址,支持带端口 | 127.0.0.1, 127.0.0.1:10008, 192.168.1.100:8080 |
配置示例
域名白名单: id.jnqj.net *.example.com app.yourdomain.com IP白名单: 127.0.0.1 127.0.0.1:10008 192.168.1.100:8080 [::1]:8080
如果您的应用部署在本地开发环境(如 127.0.0.1:8080),请将IP地址填写在IP白名单输入框中,而不是域名白名单。
3. 集成SDK或直接调用API
您可以使用我们提供的代码示例,或者直接调用REST API进行集成。
授权端点
引导用户进行授权
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| client_id | string | 是 | 应用的唯一标识(API Key) |
| redirect_uri | string | 是 | 授权成功后的回调地址 |
| response_type | string | 是 | 固定值:code |
| state | string | 否 | 用于防止CSRF攻击的随机字符串 |
响应说明
授权成功后,将重定向到回调地址,并携带以下参数:
| 参数名 | 说明 |
|---|---|
| code | 授权码,有效期10分钟 |
| state | 原样返回请求中的state参数 |
令牌端点
使用授权码换取访问令牌
重要提示
action=token 必须通过 URL 参数传递,不能放在 POST 数据中。
正确的请求地址:/api/oauth.php?action=token
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | string | 是 | 固定值:token(通过URL参数传递) |
| grant_type | string | 是 | 固定值:authorization_code |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
| code | string | 是 | 授权码 |
| redirect_uri | string | 是 | 回调地址(必须与授权时一致) |
响应示例
{
"success": true,
"message": "获取令牌成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g..."
}
}
用户信息端点
获取授权用户的基本信息
重要提示
1. action=userinfo 必须通过 URL 参数传递
2. 必须在 HTTP Header 中传入 Authorization,格式为 Authorization: Bearer {access_token}
3. 不支持通过 POST 参数传递 token,否则会返回 "缺少访问令牌" 错误
请求头
| 参数名 | 说明 |
|---|---|
| Authorization | Bearer {access_token} |
响应字段说明
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | int | 用户ID |
| string | 用户邮箱 | |
| username | string | 用户名 |
| user_type | string | 用户类型:enterprise(企业,聚焦点是统一社会信用代码)/ personal(个人,聚焦点是身份证) |
| real_name_verified | boolean | 是否已完成实名认证 |
| real_name | string | 真实姓名(实名认证后返回) |
| idcard_number | string | 身份证号码(脱敏显示,如:44xxxx********8888) |
| gender | string | 性别:男/女/未知 |
| birth_date | string | 出生日期(格式:YYYY-MM-DD) |
| nation | string | 民族 |
| city | string | 所在城市 |
| address | string | 详细地址 |
| idcard_issue_authority | string | 身份证签发机关(如:xx市公安局xx分局) |
| idcard_valid_period | string | 身份证有效期(格式:YYYY-MM-DD 至 YYYY-MM-DD) |
| enterprise | object | 企业信息(仅企业用户返回) |
企业信息字段(enterprise对象)
| 字段名 | 类型 | 说明 |
|---|---|---|
| enterprise_name | string | 企业名称 |
| enterprise_code | string | 统一社会信用代码 |
| enterprise_type | string | 企业类型 |
| registered_capital | string | 注册资本 |
| established_date | string | 成立日期 |
| business_scope | string | 经营范围 |
| verified_at | string | 企业认证时间 |
| legal_person_name | string | 法人姓名 |
响应示例 - 个人用户
{
"code": 200,
"message": "获取用户信息成功",
"data": {
"user_id": 123,
"email": "user@example.com",
"username": "用户名",
"user_type": "personal",
"real_name_verified": true,
"real_name": "张三",
"idcard_number": "44xxxx********8888",
"gender": "男",
"birth_date": "1990-01-01",
"nation": "汉",
"city": "广州市",
"address": "广东省广州市xx区xx路xx号",
"idcard_issue_authority": "广州市公安局xx分局",
"idcard_valid_period": "2020-01-01 至 2040-01-01"
}
}
响应示例 - 企业用户
{
"code": 200,
"message": "获取用户信息成功",
"data": {
"user_id": 456,
"email": "enterprise@example.com",
"username": "企业用户",
"user_type": "enterprise",
"real_name_verified": true,
"real_name": "李四",
"idcard_number": "440711********8888",
"gender": "男",
"birth_date": "1985-05-05",
"nation": "汉",
"city": "深圳市",
"address": "广东省深圳市xx区xx路xx号",
"idcard_issue_authority": "深圳市公安局xx分局",
"idcard_valid_period": "2019-01-01 至 2039-01-01",
"enterprise": {
"enterprise_name": "金牛奇迹科技有限公司",
"enterprise_code": "91440300XXXXXXXXXX",
"enterprise_type": "有限责任公司",
"registered_capital": "1000万元",
"established_date": "2020-01-01",
"business_scope": "软件开发、技术服务、技术咨询等",
"verified_at": "2024-01-15 10:30:00",
"legal_person_name": "李四"
}
}
}
错误状态码说明
| HTTP状态码 | 错误信息 | 说明 |
|---|---|---|
| 401 | 缺少访问令牌 / 无效的访问令牌 | 请求头中未携带Authorization或token已过期 |
| 403 | 账号已被禁用 | 用户账号已被管理员禁用 |
| 403 | 账号已锁定,请X分钟后重试 | 用户因多次登录失败被临时锁定 |
| 404 | 用户不存在 | token对应的用户不存在 |
刷新令牌
访问令牌有效期为2小时,过期后可以使用刷新令牌获取新的访问令牌。
刷新访问令牌
action=token 必须通过 URL 参数传递,与获取令牌使用相同的端点。
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | string | 是 | 固定值:token(通过URL参数传递) |
| grant_type | string | 是 | 固定值:refresh_token |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
| refresh_token | string | 是 | 刷新令牌 |
扫码登录
扫码登录是一种便捷的第三方登录方式,用户只需使用金牛ID App 扫描二维码即可完成授权登录,无需输入账号密码。
优势:扫码登录更加安全便捷,用户无需在第三方网站输入密码,有效防止密码泄露风险。
扫码登录流程
- 第三方应用调用
create接口创建二维码 - 第三方应用展示二维码给用户
- 用户使用金牛ID App 扫描二维码
- 用户在手机上确认授权
- 第三方应用轮询
status接口获取登录状态 - 用户确认后,第三方应用获取 access_token
- 使用 access_token 获取用户信息
扫码登录API接口
1. 创建二维码
创建扫码登录二维码
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
响应示例
{
"success": true,
"message": "二维码创建成功",
"data": {
"qr_code": "qr_a1b2c3d4e5f6...",
"qr_url": "https://id.jnqj.net/qrlogin/scan?code=qr_a1b2c3d4e5f6...",
"expires_in": 300,
"expires_at": "2026-02-04 12:05:00"
}
}
2. 查询二维码状态
查询二维码当前状态
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| qr_code | string | 是 | 二维码标识 |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
状态说明
| 状态码 | 状态文本 | 说明 |
|---|---|---|
| 0 | 待扫描 | 二维码已创建,等待用户扫描 |
| 1 | 已扫描 | 用户已扫描,等待确认 |
| 2 | 已确认 | 用户已确认授权,返回access_token |
| 3 | 已取消 | 用户取消了授权 |
| 4 | 已过期 | 二维码已过期(5分钟有效期) |
响应示例(已确认)
{
"success": true,
"data": {
"status": 2,
"status_text": "已确认",
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200
}
}
3. 获取用户信息
使用扫码登录的access_token获取用户信息
重要提示
调用此接口必须在 HTTP Header 中传入 Authorization,格式为 Authorization: Bearer {access_token}。
不支持通过 URL 参数传递 token,否则会返回 "缺少访问令牌" 错误。
请求头
| 参数名 | 说明 |
|---|---|
| Authorization | Bearer {access_token} |
响应字段说明
扫码登录获取的用户信息字段与OAuth用户信息接口完全一致:
| 字段名 | 类型 | 说明 |
|---|---|---|
| openid | string | 用户唯一标识(32位字符串) |
| real_name_verified | boolean | 是否已完成实名认证 |
| real_name | string | 真实姓名(实名认证后返回) |
| idcard_number | string | 身份证号码(脱敏显示,如:44xxxx********8888) |
| gender | string | 性别:男/女/未知 |
| birth_date | string | 出生日期(格式:YYYY-MM-DD) |
| city | string | 所在城市 |
| idcard_issue_authority | string | 身份证签发机关(如:xx市公安局xx分局) |
| idcard_valid_period | string | 身份证有效期(格式:YYYY-MM-DD 至 YYYY-MM-DD) |
响应示例
{
"success": true,
"message": "获取成功",
"data": {
"openid": "123456789012345678901234567890XX",
"real_name_verified": true,
"real_name": "真实姓名",
"idcard_number": "440711********8888",
"gender": "男",
"birth_date": "xxxx-xx-xx",
"city": "城市名称",
"idcard_issue_authority": "xx市公安局xx分局",
"idcard_valid_period": "xxxx-xx-xx 至 xxxx-xx-xx"
}
}
扫码登录SDK
我们提供了多语言的扫码登录SDK,包含完整的二维码生成、状态轮询、用户信息获取等功能。
支持的编程语言:PHP、JavaScript、Python、Java、C#、Node.js
SDK包含以下核心功能:
createQrCode()- 创建二维码queryStatus(qrCode)- 查询二维码状态pollStatus(qrCode, timeout, callback)- 轮询等待用户确认getUserInfo(accessToken)- 获取用户信息refreshToken(refreshToken)- 刷新访问令牌
技术架构概览
金牛 ID 平台采用业界领先的技术架构,以 OAuth 2.0 + OpenID Connect 作为身份认证协议,通过 RESTful API 提供标准化接口服务,依托 ELK 日志系统 实现全流程审计监控,三位一体构建安全、可靠、高效的统一身份认证体系。
金牛 ID 技术架构体系
OAuth 2.0 + OIDC
身份协议
统一认证 · 安全授权
RESTful API
接口规范
标准接入 · 灵活扩展
ELK 日志系统
审计监控
全程留痕 · 风险预警
一、身份协议:OAuth 2.0 + OpenID Connect(OIDC)
一句话定位:金牛 ID 对外 "开放登录" 的标准安全规则,让别的网站 / 应用能安全用金牛账号登录,不泄露密码。
1)OAuth 2.0(授权协议)
💡 比喻:临时门禁卡 —— 只管 "允许第三方做什么",不管 "用户是谁"
核心作用:安全授权,不暴露用户密码,权限可控
金牛场景流程:
- 合作网站 / 游戏想接入金牛登录
- 用户点击 "金牛 ID 登录" → 跳转到 id.jnqj.net
- 用户同意授权(比如 "允许获取昵称 / 头像")
- 金牛给第三方发临时令牌(Access Token)
- 第三方用令牌访问用户资源,全程看不到密码
2)OpenID Connect(OIDC,身份认证层)
💡 比喻:带防伪照片的身份证 —— 在 OAuth 2.0 基础上,明确告诉第三方 "用户是谁"
关键升级:多了 ID Token(加密 JWT),包含:
- 用户唯一 ID(sub)
- 昵称、邮箱、头像等基础信息
- 加密签名,防篡改、可验证
金牛场景:
- 第三方不仅要 "授权",还要 "知道用户身份"
- 登录成功后,金牛返回 Access Token + ID Token
- 第三方解析 ID Token,直接拿到用户身份,不用再查库
3)对金牛 ID 的业务价值
| 价值点 | 说明 |
|---|---|
| 统一账号对外输出 | 所有合作方都用同一套身份标准 |
| 安全可靠 | 密码不流出、令牌短期有效、可随时撤销 |
| 国际标准 | 接入方按国际标准开发,接入快、兼容性好 |
| 单点登录(SSO) | 一次登录,全金牛生态通行 |
二、接口:RESTful API(登录 / 授权 / 用户信息)
一句话定位:金牛 ID 对外提供的标准化 "功能调用入口",让第三方 / 内部系统能程序化调用登录、授权、获取用户信息。
1)什么是 RESTful API(大白话)
- 把所有功能看成 "资源"(用户、登录、授权)
- 用 URL 定位资源,用 HTTP 方法(GET/POST/PUT/DELETE) 表示操作
接口示例:
| HTTP 方法 | URL 路径 | 功能说明 |
|---|---|---|
POST |
/api/auth/login |
登录(创建会话) |
GET |
/api/user/info |
获取用户信息(查询) |
POST |
/api/oauth/authorize |
发起授权 |
POST |
/api/oauth/token |
令牌发放与刷新 |
特点:无状态、标准化、易扩展、前后端 / 跨系统通用
2)金牛 ID 核心接口(业务视角)
🔑 登录接口:POST /api/auth/login
| 项目 | 说明 |
|---|---|
| 输入 | 用户名 / 密码 / 验证码 |
| 输出 | Token(JWT)+ 用户基础信息 |
| 作用 | 内部系统 / 前端登录验证 |
🔓 授权接口:POST /api/oauth/authorize
| 项目 | 说明 |
|---|---|
| 输入 | client_id、redirect_uri、scope(权限范围) |
| 输出 | 授权码 / 令牌 |
| 作用 | 第三方应用获取用户授权 |
👤 用户信息接口:GET /api/user/info
| 项目 | 说明 |
|---|---|
| 输入 | Access Token |
| 输出 | 昵称、头像、邮箱、用户 ID 等 |
| 作用 | 第三方 / 内部系统获取用户资料 |
🔄 令牌校验 / 刷新:POST /api/oauth/token
| 项目 | 说明 |
|---|---|
| 作用 | 令牌过期时刷新、校验有效性 |
3)对金牛 ID 的业务价值
| 价值点 | 说明 |
|---|---|
| 标准化对接 | 所有接入方按同一套规则调用 |
| 系统解耦 | 前端 / 游戏 / AI 平台 / 第三方,都通过 API 通信 |
| 安全可控 | 所有请求带 Token,可鉴权、可限流、可审计 |
| 易于扩展 | 加新功能只加新接口,不影响现有系统 |
三、日志:ELK / 自研日志系统(登录审计、异常监控)
一句话定位:金牛 ID 的 "黑匣子 + 监控大屏",记录所有登录 / 授权行为,可追溯、可告警、可排查问题。
1)ELK 是什么(大白话)
| 组件 | 全称 | 作用比喻 |
|---|---|---|
| E | Elasticsearch | 存日志、秒级搜索(像智能仓库) |
| L | Logstash | 收集、清洗、格式化日志(像数据搬运工) |
| K | Kibana | 可视化、做报表、监控大屏(像仪表盘) |
说明:也可能是自研日志系统,但逻辑一样:采集 → 存储 → 分析 → 告警
2)金牛 ID 日志记录什么(核心审计字段)
每条日志必含:
| 字段 | 说明 | 示例 |
|---|---|---|
| Who | 用户 ID / 用户名、操作人 | user_id: 12345, username: 张三 |
| What | 登录 / 授权成功 / 失败、授权范围 | action: login, result: success |
| When | 精确时间戳 | 2026-02-17 14:30:25.123 |
| Where | IP 地址、地理位置、设备 / 浏览器 | ip: 113.88.XX.XX, device: Chrome 120 |
| Result | 成功 / 失败、错误码、令牌 ID | status: 200, token_id: eyJhbG... |
3)两大核心业务作用
📋 登录审计(可追溯)
- 谁、何时、何地、用什么设备登录
- 授权给谁、授权了什么权限
- 出问题可回溯每一步操作,定位责任
🚨 异常监控(安全告警)
规则示例:
| 监控规则 | 触发条件 | 自动响应 |
|---|---|---|
| 暴力破解检测 | 1 小时内 5 次密码错误 | 锁定账号 / 告警 |
| 异地登录检测 | 常用深圳 → 突然北京 | 二次验证 / 通知用户 |
| 异常请求检测 | 大量异常授权请求 | 限流 / 拉黑 IP |
作用:防暴力破解、防盗号、防恶意授权
4)对金牛 ID 的业务价值
| 价值点 | 说明 |
|---|---|
| 安全底线 | 所有身份操作留痕、可查、可追溯 |
| 合规保障 | 满足账号安全、用户隐私审计要求 |
| 快速排障 | 登录失败、授权异常,秒级定位原因 |
| 风险预警 | 异常行为自动告警,提前止损 |
四、三者在金牛 ID 中的关系(业务闭环)
金牛 ID 业务流程闭环
🎯 一句话总结
OAuth 2.0/OIDC 定安全规则,RESTful API 做功能入口,ELK 日志 做安全兜底,
三位一体支撑金牛 ID 的统一身份与开放授权业务。
企业用户应用管理
一句话定位:企业用户通过应用管理系统,可以创建和管理多个应用,每个应用对应独立的 API 令牌,实现精细化的权限控制和业务隔离。
💡 核心概念:应用是令牌的上级容器,必须先创建应用,才能在应用下创建令牌。每个应用代表一个独立的业务系统或第三方接入方。
应用管理架构
| 层级 | 对象 | 数量限制 | 说明 |
|---|---|---|---|
| 1级 | 企业用户 | 1个 | 完成企业认证的企业账号 |
| 2级 | 应用 | 最多300个 | 每个企业用户可创建的应用数量上限 |
| 3级 | 令牌 | 每个应用1个 | 每个应用只能创建一个 API 令牌 |
应用场景示例
- 多业务线管理:企业有多个业务系统(官网、APP、小程序),每个系统创建一个独立应用
- 多环境隔离:开发环境、测试环境、生产环境分别创建不同应用,避免令牌混用
- 第三方接入:为不同的合作伙伴创建独立应用,便于权限管理和审计追踪
- 项目制管理:按项目维度创建应用,项目结束后可单独禁用或删除
创建应用
企业用户登录后,进入应用管理页面,按照以下步骤创建新应用:
创建流程
应用创建步骤
应用信息字段说明
| 字段名 | 必填 | 说明 | 示例 |
|---|---|---|---|
| 应用名称 | 是 | 应用的显示名称,建议体现业务含义 | "官网登录系统" |
| 应用描述 | 否 | 应用的详细说明,便于后续管理 | "用于公司官网用户登录认证" |
| 回调域名 | 是 | 允许回调的域名白名单,多个用换行分隔 | example.com |
| 回调IP | 否 | 允许回调的IP白名单,支持带端口 | 127.0.0.1:8080 |
⚠️ 重要提示:应用名称创建后不可修改,请谨慎命名。建议采用"业务系统+环境"的命名规范,如"官网-生产环境"。
应用列表管理
企业用户可以在应用列表页面查看和管理所有已创建的应用:
应用列表功能
| 功能 | 说明 |
|---|---|
| 查看详情 | 查看应用的基本信息、回调配置、令牌状态 |
| 编辑配置 | 修改应用描述、回调域名/IP白名单 |
| 创建令牌 | 为应用创建 API 令牌(每个应用限1个) |
| 禁用/启用 | 临时禁用应用,禁用后该应用的令牌失效 |
| 删除应用 | 删除应用及其关联的令牌(不可恢复) |
应用状态说明
| 状态 | 图标 | 说明 |
|---|---|---|
| 正常 | 🟢 | 应用正常运行,令牌有效 |
| 已禁用 | ⚫ | 应用被禁用,令牌失效 |
| 无令牌 | 🟡 | 应用已创建但未生成令牌 |
创建令牌
应用创建成功后,需要为应用创建 API 令牌才能进行接口调用。每个应用只能创建一个令牌。
令牌创建流程
令牌创建步骤
令牌信息说明
| 字段 | 说明 | 使用场景 |
|---|---|---|
| API Key | 客户端标识(Client ID) | 授权请求、令牌交换、用户信息获取 |
| API Secret | 客户端密钥(Client Secret) | 服务器端令牌交换(严禁前端暴露) |
🔐 安全警告:API Secret 仅在创建时显示一次,请务必立即保存。如遗失需要删除旧令牌并重新创建。
令牌管理
令牌创建后,企业用户可以在应用详情页查看和管理令牌状态:
令牌管理操作
| 操作 | 说明 | 影响 |
|---|---|---|
| 查看 API Key | 随时查看 API Key(Client ID) | 无影响,可公开使用 |
| 重置令牌 | 删除旧令牌,创建新令牌 | 旧令牌立即失效,需更新所有接入方配置 |
| 删除令牌 | 删除当前应用的令牌 | 该应用的所有接口调用将失败 |
最佳实践
- 定期轮换:建议每 90 天重置一次令牌,降低密钥泄露风险
- 环境隔离:不同环境(开发/测试/生产)使用不同应用和令牌
- 权限最小化:按业务需求配置回调白名单,避免使用通配符
- 安全存储:API Secret 存储在服务器环境变量或密钥管理系统中
- 监控告警:关注令牌的使用情况和异常调用日志
限制说明
| 限制项 | 上限 | 说明 |
|---|---|---|
| 应用数量 | 300个 | 每个企业用户最多可创建的应用数量 |
| 令牌数量 | 1个/应用 | 每个应用只能创建一个令牌 |
| 回调域名 | 50个 | 每个应用最多配置的域名白名单数量 |
| 回调IP | 50个 | 每个应用最多配置的IP白名单数量 |
📝 快速开始 checklist
✅ 完成企业认证
✅ 创建应用并命名
✅ 配置回调白名单
✅ 创建 API 令牌
✅ 保存 API Secret
✅ 开始接口对接
重要提示:API地址配置
第三方网站必须配置完整的金牛ID服务器地址,不能使用相对路径。
- 正式环境:
https://id.jnqj.net - 错误示例:
/api/oauth.php(缺少域名会导致404错误) - 正确示例:
https://id.jnqj.net/api/oauth.php
PHP 示例
<?php
// 金牛ID OAuth 登录集成示例
// 请替换为您的API密钥
$apiKey = 'your_api_key_here';
$apiSecret = 'your_api_secret_here';
// 请替换为您的回调地址
$redirectUri = 'https://your-domain.com/callback.php';
class JnqjOAuth {
private $apiKey;
private $apiSecret;
private $baseUrl = 'https://id.jnqj.net';
public function __construct($apiKey, $apiSecret) {
$this->apiKey = $apiKey;
$this->apiSecret = $apiSecret;
}
// 获取授权URL
public function getAuthUrl($redirectUri, $state = '') {
$params = [
'client_id' => $this->apiKey,
'redirect_uri' => $redirectUri,
'response_type' => 'code',
'state' => $state,
];
return $this->baseUrl . '/oauth/authorize?' . http_build_query($params);
}
// 获取访问令牌
public function getAccessToken($code, $redirectUri) {
$data = [
'grant_type' => 'authorization_code',
'client_id' => $this->apiKey,
'client_secret' => $this->apiSecret,
'code' => $code,
'redirect_uri' => $redirectUri,
];
// action=token 必须通过 URL 参数传递
$ch = curl_init($this->baseUrl . '/api/oauth.php?action=token');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// 获取用户信息
public function getUserInfo($accessToken) {
$ch = curl_init($this->baseUrl . '/api/oauth.php?action=userinfo');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
}
Python 示例
import requests
import urllib.parse
class JnqjOAuth:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
self.base_url = 'https://id.jnqj.net'
# 获取授权URL
def get_auth_url(self, redirect_uri, state=''):
params = {
'client_id': self.api_key,
'redirect_uri': redirect_uri,
'response_type': 'code',
'state': state,
}
return f"{self.base_url}/oauth/authorize?{urllib.parse.urlencode(params)}"
# 获取访问令牌
def get_access_token(self, code, redirect_uri):
data = {
'grant_type': 'authorization_code',
'client_id': self.api_key,
'client_secret': self.api_secret,
'code': code,
'redirect_uri': redirect_uri,
}
# action=token 必须通过 URL 参数传递
response = requests.post(
f"{self.base_url}/api/oauth.php?action=token",
data=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
return response.json()
# 获取用户信息
def get_user_info(self, access_token):
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get(
f"{self.base_url}/api/oauth.php?action=userinfo",
headers=headers
)
return response.json()
# 刷新令牌
def refresh_token(self, refresh_token):
data = {
'grant_type': 'refresh_token',
'client_id': self.api_key,
'client_secret': self.api_secret,
'refresh_token': refresh_token,
}
# action=token 必须通过 URL 参数传递
response = requests.post(
f"{self.base_url}/api/oauth.php?action=token",
data=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
return response.json()
# Flask 完整示例
from flask import Flask, request, redirect, session
app = Flask(__name__)
app.secret_key = 'your-secret-key'
oauth = JnqjOAuth('your-api-key', 'your-api-secret')
REDIRECT_URI = 'http://localhost:5000/callback'
@app.route('/login')
def login():
import secrets
state = secrets.token_urlsafe(16)
session['oauth_state'] = state
auth_url = oauth.get_auth_url(REDIRECT_URI, state)
return redirect(auth_url)
@app.route('/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
# 验证 state 防止 CSRF 攻击
if state != session.get('oauth_state'):
return 'Invalid state', 400
# 获取访问令牌
token_data = oauth.get_access_token(code, REDIRECT_URI)
if token_data.get('success'):
session['access_token'] = token_data['data']['access_token']
return redirect('/profile')
return 'Authentication failed', 400
@app.route('/profile')
def profile():
access_token = session.get('access_token')
if not access_token:
return redirect('/login')
user_info = oauth.get_user_info(access_token)
return user_info
Java 示例
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class JnqjOAuth {
private String apiKey;
private String apiSecret;
private String baseUrl = "https://id.jnqj.net";
private Gson gson = new Gson();
public JnqjOAuth(String apiKey, String apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}
// 获取授权URL
public String getAuthUrl(String redirectUri, String state) throws Exception {
Map<String, String> params = new HashMap<>();
params.put("client_id", apiKey);
params.put("redirect_uri", redirectUri);
params.put("response_type", "code");
params.put("state", state);
String queryString = params.entrySet().stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
return baseUrl + "/oauth/authorize?" + queryString;
}
// 获取访问令牌
public JsonObject getAccessToken(String code, String redirectUri) throws Exception {
String url = baseUrl + "/api/oauth.php";
Map<String, String> params = new HashMap<>();
params.put("grant_type", "authorization_code");
params.put("client_id", apiKey);
params.put("client_secret", apiSecret);
params.put("code", code);
params.put("redirect_uri", redirectUri);
String postData = params.entrySet().stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
return sendPostRequest(url, postData);
}
// 获取用户信息
public JsonObject getUserInfo(String accessToken) throws Exception {
String url = baseUrl + "/api/oauth.php?action=userinfo";
return sendGetRequest(url, accessToken);
}
// 刷新令牌
public JsonObject refreshToken(String refreshToken) throws Exception {
String url = baseUrl + "/api/oauth.php";
Map<String, String> params = new HashMap<>();
params.put("grant_type", "refresh_token");
params.put("client_id", apiKey);
params.put("client_secret", apiSecret);
params.put("refresh_token", refreshToken);
String postData = params.entrySet().stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
return sendPostRequest(url, postData);
}
private JsonObject sendPostRequest(String urlString, String postData) throws Exception {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
try (DataOutputStream wr = new DataOutputStream(conn.getOutputStream())) {
wr.writeBytes(postData);
wr.flush();
}
return parseResponse(conn);
}
private JsonObject sendGetRequest(String urlString, String accessToken) throws Exception {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
return parseResponse(conn);
}
private JsonObject parseResponse(HttpURLConnection conn) throws Exception {
BufferedReader in = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
return gson.fromJson(response.toString(), JsonObject.class);
}
}
// Spring Boot 完整示例
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpSession;
@RestController
public class OAuthController {
private final JnqjOAuth oauth = new JnqjOAuth("your-api-key", "your-api-secret");
private final String REDIRECT_URI = "http://localhost:8080/callback";
@GetMapping("/login")
public RedirectView login(HttpSession session) throws Exception {
String state = java.util.UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
String authUrl = oauth.getAuthUrl(REDIRECT_URI, state);
return new RedirectView(authUrl);
}
@GetMapping("/callback")
public String callback(@RequestParam String code,
@RequestParam String state,
HttpSession session) throws Exception {
// 验证 state
if (!state.equals(session.getAttribute("oauth_state"))) {
return "Invalid state";
}
// 获取访问令牌
JsonObject tokenData = oauth.getAccessToken(code, REDIRECT_URI);
if (tokenData.get("success").getAsBoolean()) {
String accessToken = tokenData.getAsJsonObject("data")
.get("access_token").getAsString();
session.setAttribute("access_token", accessToken);
return "redirect:/profile";
}
return "Authentication failed";
}
@GetMapping("/profile")
public JsonObject profile(HttpSession session) throws Exception {
String accessToken = (String) session.getAttribute("access_token");
if (accessToken == null) {
throw new RuntimeException("Not authenticated");
}
return oauth.getUserInfo(accessToken);
}
}
Node.js 示例
const axios = require('axios');
const querystring = require('querystring');
class JnqjOAuth {
constructor(apiKey, apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
this.baseUrl = 'https://id.jnqj.net';
}
// 获取授权URL
getAuthUrl(redirectUri, state = '') {
const params = new URLSearchParams({
client_id: this.apiKey,
redirect_uri: redirectUri,
response_type: 'code',
state: state,
});
return `${this.baseUrl}/oauth/authorize?${params.toString()}`;
}
// 获取访问令牌
async getAccessToken(code, redirectUri) {
const data = {
grant_type: 'authorization_code',
client_id: this.apiKey,
client_secret: this.apiSecret,
code: code,
redirect_uri: redirectUri,
};
const response = await axios.post(
`${this.baseUrl}/api/oauth.php`,
querystring.stringify(data),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data;
}
// 获取用户信息
async getUserInfo(accessToken) {
const response = await axios.get(
`${this.baseUrl}/api/oauth.php?action=userinfo`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
return response.data;
}
// 刷新令牌
async refreshToken(refreshToken) {
const data = {
grant_type: 'refresh_token',
client_id: this.apiKey,
client_secret: this.apiSecret,
refresh_token: refreshToken,
};
const response = await axios.post(
`${this.baseUrl}/api/oauth.php`,
querystring.stringify(data),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data;
}
}
// Express 完整示例
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: true
}));
const oauth = new JnqjOAuth('your-api-key', 'your-api-secret');
const REDIRECT_URI = 'http://localhost:3000/callback';
// 登录路由
app.get('/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = oauth.getAuthUrl(REDIRECT_URI, state);
res.redirect(authUrl);
});
// 回调路由
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// 验证 state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
try {
// 获取访问令牌
const tokenData = await oauth.getAccessToken(code, REDIRECT_URI);
if (tokenData.success) {
req.session.accessToken = tokenData.data.access_token;
res.redirect('/profile');
} else {
res.status(400).send('Authentication failed');
}
} catch (error) {
res.status(500).send(error.message);
}
});
// 用户信息路由
app.get('/profile', async (req, res) => {
const accessToken = req.session.accessToken;
if (!accessToken) {
return res.redirect('/login');
}
try {
const userInfo = await oauth.getUserInfo(accessToken);
res.json(userInfo);
} catch (error) {
res.status(500).send(error.message);
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
其他语言示例
Go 示例
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type JnqjOAuth struct {
APIKey string
APISecret string
BaseURL string
}
func NewJnqjOAuth(apiKey, apiSecret string) *JnqjOAuth {
return &JnqjOAuth{
APIKey: apiKey,
APISecret: apiSecret,
BaseURL: "https://id.jnqj.net",
}
}
// 获取授权URL
func (o *JnqjOAuth) GetAuthUrl(redirectUri, state string) string {
params := url.Values{}
params.Set("client_id", o.APIKey)
params.Set("redirect_uri", redirectUri)
params.Set("response_type", "code")
params.Set("state", state)
return fmt.Sprintf("%s/oauth/authorize?%s", o.BaseURL, params.Encode())
}
// 获取访问令牌
func (o *JnqjOAuth) GetAccessToken(code, redirectUri string) (map[string]interface{}, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", o.APIKey)
data.Set("client_secret", o.APISecret)
data.Set("code", code)
data.Set("redirect_uri", redirectUri)
resp, err := http.Post(
o.BaseURL+"/api/oauth.php",
"application/x-www-form-urlencoded",
strings.NewReader(data.Encode()),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
// 获取用户信息
func (o *JnqjOAuth) GetUserInfo(accessToken string) (map[string]interface{}, error) {
req, _ := http.NewRequest("GET", o.BaseURL+"/api/oauth.php?action=userinfo", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
C# / .NET 示例
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
public class JnqjOAuth
{
private string _apiKey;
private string _apiSecret;
private string _baseUrl = "https://id.jnqj.net";
private HttpClient _httpClient;
public JnqjOAuth(string apiKey, string apiSecret)
{
_apiKey = apiKey;
_apiSecret = apiSecret;
_httpClient = new HttpClient();
}
// 获取授权URL
public string GetAuthUrl(string redirectUri, string state)
{
var queryParams = new Dictionary<string, string>
{
["client_id"] = _apiKey,
["redirect_uri"] = redirectUri,
["response_type"] = "code",
["state"] = state
};
var queryString = string.Join("&", queryParams);
return $"{_baseUrl}/oauth/authorize?{queryString}";
}
// 获取访问令牌
public async Task<JsonDocument> GetAccessTokenAsync(string code, string redirectUri)
{
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["client_id"] = _apiKey,
["client_secret"] = _apiSecret,
["code"] = code,
["redirect_uri"] = redirectUri
});
var response = await _httpClient.PostAsync($"{_baseUrl}/api/oauth.php", content);
var responseString = await response.Content.ReadAsStringAsync();
return JsonDocument.Parse(responseString);
}
// 获取用户信息
public async Task<JsonDocument> GetUserInfoAsync(string accessToken)
{
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var response = await _httpClient.GetAsync($"{_baseUrl}/api/oauth.php?action=userinfo");
var responseString = await response.Content.ReadAsStringAsync();
return JsonDocument.Parse(responseString);
}
}
Ruby 示例
require 'net/http'
require 'uri'
require 'json'
class JnqjOAuth
def initialize(api_key, api_secret)
@api_key = api_key
@api_secret = api_secret
@base_url = 'https://id.jnqj.net'
end
# 获取授权URL
def get_auth_url(redirect_uri, state = '')
params = {
'client_id' => @api_key,
'redirect_uri' => redirect_uri,
'response_type' => 'code',
'state' => state
}
"#{@base_url}/oauth/authorize?#{URI.encode_www_form(params)}"
end
# 获取访问令牌
def get_access_token(code, redirect_uri)
uri = URI("#{@base_url}/api/oauth.php")
req = Net::HTTP::Post.new(uri)
req.set_form_data(
'grant_type' => 'authorization_code',
'client_id' => @api_key,
'client_secret' => @api_secret,
'code' => code,
'redirect_uri' => redirect_uri
)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
JSON.parse(res.body)
end
# 获取用户信息
def get_user_info(access_token)
uri = URI("#{@base_url}/api/oauth.php?action=userinfo")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{access_token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
JSON.parse(res.body)
end
end
# Rails 完整示例
class SessionsController < ApplicationController
def new
oauth = JnqjOAuth.new(ENV['JNQJ_API_KEY'], ENV['JNQJ_API_SECRET'])
state = SecureRandom.hex(16)
session[:oauth_state] = state
redirect_to oauth.get_auth_url(callback_url, state)
end
def create
# 验证 state
if params[:state] != session[:oauth_state]
return redirect_to root_path, alert: 'Invalid state'
end
oauth = JnqjOAuth.new(ENV['JNQJ_API_KEY'], ENV['JNQJ_API_SECRET'])
token_data = oauth.get_access_token(params[:code], callback_url)
if token_data['success']
session[:access_token] = token_data['data']['access_token']
redirect_to profile_path
else
redirect_to root_path, alert: 'Authentication failed'
end
end
private
def callback_url
url_for(action: 'create', only_path: false)
end
end
Rust 示例
use reqwest;
use serde_json::Value;
use std::collections::HashMap;
pub struct JnqjOAuth {
api_key: String,
api_secret: String,
base_url: String,
}
impl JnqjOAuth {
pub fn new(api_key: String, api_secret: String) -> Self {
JnqjOAuth {
api_key,
api_secret,
base_url: "https://id.jnqj.net".to_string(),
}
}
// 获取授权URL
pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String {
format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&state={}",
self.base_url, self.api_key, redirect_uri, state
)
}
// 获取访问令牌
pub async fn get_access_token(&self, code: &str, redirect_uri: &str) -> Result<Value, reqwest::Error> {
let client = reqwest::Client::new();
let mut params = HashMap::new();
params.insert("grant_type", "authorization_code");
params.insert("client_id", &self.api_key);
params.insert("client_secret", &self.api_secret);
params.insert("code", code);
params.insert("redirect_uri", redirect_uri);
let res = client
.post(&format!("{}/api/oauth.php", self.base_url))
.form(¶ms)
.send()
.await?;
res.json().await
}
// 获取用户信息
pub async fn get_user_info(&self, access_token: &str) -> Result<Value, reqwest::Error> {
let client = reqwest::Client::new();
let res = client
.get(&format!("{}/api/oauth.php?action=userinfo", self.base_url))
.bearer_auth(access_token)
.send()
.await?;
res.json().await
}
}
错误码
| 错误码 | 说明 |
|---|---|
| 400 | 请求参数错误 |
| 401 | 未授权,访问令牌无效或过期 |
| 403 | 禁止访问,权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
常见错误信息
| 错误信息 | 说明 | 解决方法 |
|---|---|---|
| 回调地址不在允许的域名/IP列表中 | redirect_uri 不在白名单中 | 在API密钥管理页面添加回调地址到白名单 |
| 无效的客户端ID | client_id 错误或不存在 | 检查API Key是否正确 |
| 不支持的响应类型 | response_type 不是 code | 确保 response_type=code |
| 授权码无效或已过期 | code 错误或已使用 | 重新获取授权码 |
安全建议
重要提示:请妥善保管您的API Secret,不要在客户端代码(如JavaScript、移动应用)中暴露API Secret。所有涉及API Secret的操作应在服务器端完成。
回调地址白名单
为了防止恶意应用滥用您的API密钥,强烈建议配置回调地址白名单:
- 在API密钥管理页面配置允许的回调地址
- 只添加您实际使用的域名或IP地址
- 生产环境建议使用HTTPS域名
- 本地开发可以使用127.0.0.1或localhost
- 支持通配符域名(如*.example.com)来匹配子域名
通用安全建议
- 使用HTTPS协议进行所有API调用
- 妥善存储API Secret,建议使用环境变量或密钥管理服务
- 实现适当的访问令牌刷新机制
- 验证state参数以防止CSRF攻击
- 定期检查API使用日志,发现异常及时处理
常见问题
Q: 授权码有效期是多久?
A: 授权码有效期为10分钟,且只能使用一次。
Q: 访问令牌过期了怎么办?
A: 可以使用刷新令牌获取新的访问令牌,无需用户再次授权。
Q: 刷新令牌也过期了怎么办?
A: 需要重新引导用户进行授权流程。
Q: 如何获取用户的手机号?
A: 出于隐私保护,金牛ID不提供用户手机号获取接口。
Q: 为什么提示"回调地址不在允许的域名/IP列表中"?
A: 为了安全起见,金牛ID要求所有回调地址必须在白名单中注册。请前往API密钥管理页面,在"回调地址白名单"部分添加您的回调地址。
Q: 如何配置本地开发环境的回调地址?
A: 在回调地址白名单中添加:
- IP格式:
127.0.0.1或127.0.0.1:8080(带端口) - 域名格式:
localhost
Q: 通配符域名如何使用?
A: 使用 *.example.com 可以匹配所有子域名,如 app.example.com、api.example.com 等。注意:通配符只匹配子域名,不匹配主域名本身。
Q: 配置白名单后多久生效?
A: 配置立即生效,无需等待。
如有其他问题,请联系客服:Jenico.Li | Jenico@vip.qq.com