金牛ID OAuth 开发文档
金牛ID提供标准的OAuth 2.0授权服务,让第三方应用可以快速接入金牛ID账号体系,实现一键登录功能。
5分钟快速接入金牛ID登录
无需深厚的OAuth知识,按照下面的步骤操作,5分钟即可完成接入
支持 PHP / Python / Java / Node.js / C# / Go5分钟快速接入(傻瓜式教程)
本教程将带你从零开始,一步步完成金牛ID登录的接入。每个步骤都有完整的代码,直接复制就能用。
金牛ID登录就像微信扫码登录一样:你的网站不需要自己管理用户密码,用户点击"金牛ID登录"按钮后,跳到金牛ID的页面输入账号密码,登录成功后自动跳回你的网站,你的网站就能拿到用户信息了。
认证通过后,进入API密钥管理页面,系统会自动为你生成API密钥。
你需要记录以下两个关键信息:
| 名称 | 说明 | 示例 |
|---|---|---|
| API Key | 应用的唯一标识(相当于账号) | jn_abc123def456... |
| API Secret | 应用的密钥(相当于密码,务必保密) | sk_xyz789ghi012... |
重要提醒:API Secret 就像你的银行卡密码一样,千万不要放在前端代码中,千万不要提交到GitHub等代码仓库,千万不要告诉任何人。
在API密钥管理页面的"回调地址白名单"区域,添加你的网站域名或IP。
域名白名单(用于线上环境,精确匹配,不支持通配符):
www.yourwebsite.com app.yourwebsite.com
IP白名单(用于内网测试,支持IP+端口):
127.0.0.1:8080 192.168.1.100:3000 10.0.0.5:5000
白名单就像小区的门禁卡。只有在白名单里的地址,金牛ID才允许登录成功后跳转过去。这是为了防止坏人伪造你的回调地址偷用户信息。内网开发时填IP+端口就行。
在你的登录页面添加一个按钮,点击后跳转到金牛ID授权页面。把下面的代码中的YOUR_API_KEY和YOUR_CALLBACK_URL换成你自己的。
<!-- HTML: 金牛ID登录按钮 --> <a href="https://id.jnqj.net/oauth/authorize?response_type=code&client_id=YOUR_API_KEY&redirect_uri=https%3A%2F%2Fwww.yourwebsite.com%2Fcallback.php&state=随机字符串&scope=basic profile" style="display:inline-block;background:linear-gradient(135deg,#C2B59B,#8B7355);color:#fff;padding:12px 32px;border-radius:8px;text-decoration:none;font-size:16px;"> 🔑 金牛ID登录</a>
用户点击这个按钮后,浏览器会跳到金牛ID的登录页面。用户在金牛ID的页面输入账号密码(你不需要也不能看到密码),登录成功后金牛ID会自动跳回你的网站。
state参数怎么生成?生成一个随机字符串就行,目的是防止黑客伪造请求:
// PHP 生成 state$state = bin2hex(random_bytes(16)); // 生成32位随机字符串$_SESSION['oauth_state'] = $state; // 存到session里,后面验证用
// Python 生成 stateimport secrets, session
state = secrets.token_hex(16)
session['oauth_state'] = state
// Node.js 生成 stateconst crypto = require('crypto');
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;用户在金牛ID登录成功后,金牛ID会跳回你配置的回调地址,并带上一个code参数(授权码)。你需要用这个code去换access_token(访问令牌)。
以下是完整的 PHP 回调处理代码,直接复制可用:
<?php
// callback.php - 金牛ID登录回调处理
session_start();
// ====== 配置(换成你自己的) ======
$API_KEY = 'YOUR_API_KEY'; // 你的API Key$API_SECRET = 'YOUR_API_SECRET'; // 你的API Secret$REDIRECT_URI = 'https://www.yourwebsite.com/callback.php'; // 回调地址(必须和白名单一致)// =================================
// 1. 检查是否有错误(用户拒绝授权等)if (isset($_GET['error'])) {
die('登录失败: ' . htmlspecialchars($_GET['error_description'] ?? $_GET['error']));
}
// 2. 验证state(防止CSRF攻击,非常重要!)if (!isset($_GET['state']) || $_GET['state'] !== ($_SESSION['oauth_state'] ?? '')) {
die('安全验证失败:state不匹配,请重试');
}
// 3. 获取授权码$code = $_GET['code'] ?? '';
if (empty($code)) {
die('未收到授权码');
}
// 4. 用授权码换取访问令牌(后端请求,API Secret不会暴露给前端)$ch = curl_init('https://id.jnqj.net/api/oauth.php?action=token');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'authorization_code',
'client_id' => $API_KEY,
'client_secret' => $API_SECRET,
'code' => $code,
'redirect_uri' => $REDIRECT_URI,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 启用SSL证书验证(必须)curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 验证主机名(必须为2)
$response = curl_exec($ch);
curl_close($ch);
$tokenData = json_decode($response, true);
// 5. 检查是否成功获取令牌if (!isset($tokenData['access_token'])) {
die('获取令牌失败: ' . ($tokenData['message'] ?? $response));
}
// 6. 保存令牌到Session$_SESSION['access_token'] = $tokenData['access_token'];
$_SESSION['refresh_token'] = $tokenData['refresh_token'] ?? '';
echo '<h1>✅ 登录成功!</h1>';
echo '<p>正在获取用户信息...</p>';
echo '<script>setTimeout(() => location.href = "userinfo.php", 1000);</script>';
?>这一步就像用电影票换入场手环:用户登录后金牛ID给你一张"电影票"(授权码code),你拿着票去窗口换"手环"(access_token)。有了手环,你就能进入影院(获取用户信息)了。注意:换手环的过程必须在你的后端完成,因为需要用到API Secret。
拿到 access_token 后,就可以调用用户信息接口获取登录用户的资料了:
<?php
// userinfo.php - 获取金牛ID用户信息
session_start();
$accessToken = $_SESSION['access_token'] ?? '';
if (empty($accessToken)) {
die('请先登录');
}
// 调用用户信息接口$ch = curl_init('https://id.jnqj.net/api/oauth.php?action=userinfo');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken, // 注意:令牌放在Header里]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 启用SSL证书验证(必须)curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 验证主机名(必须为2)
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result['success'] ?? false) {
$user = $result['data'];
// 打印用户信息 echo '<h1>欢迎, ' . htmlspecialchars($user['username'] ?? '') . '</h1>';
echo '<p>邮箱: ' . htmlspecialchars($user['email'] ?? '') . '</p>';
echo '<p>OpenID: ' . htmlspecialchars($user['openid'] ?? '') . '</p>';
echo '<p>用户类型: ' . htmlspecialchars($user['user_type'] ?? '') . '</p>';
echo '<p>实名认证: ' . (($user['real_name_verified'] ?? false) ? '已认证' : '未认证') . '</p>';
if ($user['user_type'] === 'personal') {
// 个人用户信息 echo '<p>姓名: ' . htmlspecialchars($user['real_name'] ?? '') . '</p>';
echo '<p>性别: ' . htmlspecialchars($user['gender'] ?? '') . '</p>';
} elseif ($user['user_type'] === 'enterprise') {
// 企业用户信息 echo '<p>企业名称: ' . htmlspecialchars($user['enterprise']['enterprise_name'] ?? '') . '</p>';
echo '<p>统一社会信用代码: ' . htmlspecialchars($user['enterprise']['enterprise_code'] ?? '') . '</p>';
}
} else {
echo '获取用户信息失败: ' . ($result['message'] ?? '未知错误');
}
?>这一步就是用门禁卡刷卡进入。access_token就是你的临时门禁卡(2小时有效),用它来访问金牛ID的用户信息接口,就能拿到当前登录用户的基本资料了。
🎉 恭喜!到这里你已经完成了金牛ID登录接入!
- 用户点击"金牛ID登录"按钮跳转到金牛ID授权页面
- 用户在金牛ID页面登录并授权
- 金牛ID带着授权码跳回你的回调地址
- 你的后端用授权码换取access_token
- 用access_token获取用户信息
- 在你的网站中创建用户会话,登录完成
流程图解(看图就懂)
下面用一张图说明整个登录流程,蓝色是用户,金色是金牛ID,绿色是你的应用:
① 用户点击"金牛ID登录"按钮
你的网站将用户引导到https://id.jnqj.net/oauth/authorize,携带 client_id、redirect_uri、state 等参数
② 用户在金牛ID页面登录
用户在金牛ID的页面输入账号密码(你的网站看不到密码),金牛ID验证身份
用户操作时间③ 用户同意授权
金牛ID显示授权确认页,用户点击"允许"后,金牛ID生成授权码(code)
用户操作时间④ 金牛ID跳回你的网站
金牛ID重定向到你的 redirect_uri,携带code(授权码)和state参数
⑤ 你的后端用code换token
你的服务器后端用授权码 + API Key + API Secret 调用/api/oauth.php?action=token,获取 access_token 和 refresh_token
⑥ 你的后端获取用户信息
用 access_token 调用/api/oauth.php?action=userinfo,获取用户的邮箱、姓名、认证状态等信息
⑦ 登录完成!
你的网站创建用户会话,用户成功登录。后续可使用 refresh_token 刷新令牌(30天内有效)
完成核心概念(大白话版)
如果你是第一次接触OAuth,以下概念可能对你有帮助。如果你已经熟悉OAuth 2.0,可以直接跳到API参考。
接入前检查清单
在开始编码之前,请确认以下事项都已完成:
📋 账号准备
- 已注册金牛ID账号
- 已完成企业实名认证(个人认证无法创建API应用)
- 已登录API密钥管理页面获取了 API Key 和 API Secret
📋 白名单配置
- 已将你的网站域名添加到域名白名单(如 www.yourwebsite.com)
- 如果是内网开发,已将IP+端口添加到IP白名单(如 127.0.0.1:8080)
- 回调地址的协议(https/http)、域名、端口、路径与白名单完全一致
- 已配置回调地址白名单(必须),空白名单将拒绝所有请求
- 回调地址使用HTTPS(生产环境必须,localhost开发环境除外)
📋 代码准备
- 后端环境支持 cURL 或类似的 HTTP 请求库
- 已准备好 Session 或其他状态存储机制(用于保存 state 和 token)
- API Secret 保存在后端,不会暴露给前端
- 已实现回调页面(callback.php),能接收 code 参数
- 所有 cURL 请求已启用 SSL 证书验证(CURLOPT_SSL_VERIFYPEER=true)
📋 安全检查
- state 参数使用安全的随机数生成(不小于16字节)
- 回调时验证 state 与发送时一致
- access_token 通过 HTTP Header 传递,不放在 URL 参数中
- HTTPS 部署(生产环境必须)
- 已了解各端点速率限制(登录20次/5分钟、注册5次/小时、密码修改10次/10分钟、OAuth令牌10次/分钟)
- 已了解CSRF令牌机制(Web表单POST请求需要携带CSRF令牌)
- 已了解安全图片需认证访问(身份证等敏感图片需登录+HMAC签名URL,5分钟有效)
- client_secret 不通过GET参数传输(使用POST请求体或Authorization头)
OAuth 2.0 授权流程
金牛ID采用OAuth 2.0标准的授权码模式(Authorization Code Flow),流程如下:
OAuth 2.0 授权码模式就是:用户去金牛ID登录 → 金牛ID给你一个"临时通行证"(code) → 你用通行证换"门禁卡"(access_token) → 用门禁卡获取用户信息。整个过程你的网站永远不会接触到用户的密码,非常安全。
- 第三方应用引导用户访问金牛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- 错误原因代码(部分错误提供)state- 原样返回请求中的state参数(必填,始终返回)
安全提示:出于最小信息披露原则,错误回调中不再包含user_type字段。如需区分用户类型,请通过用户信息端点(userinfo)获取。
接入指南
用户类型说明
金牛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密钥管理页面提供两个独立的输入框,请分别配置:
| 输入框 | 支持的格式 | 示例 |
|---|---|---|
| 域名白名单 | 精确域名(不支持通配符) | id.jnqj.net, app.yourdomain.com, www.example.com |
| IP白名单 | IP地址,支持带端口(含内网IP) | 127.0.0.1, 127.0.0.1:10008, 192.168.1.100:8080, 10.0.0.5:3000 |
配置示例
域名白名单(精确匹配,每行一个域名):id.jnqj.net app.yourdomain.com www.example.com IP白名单(支持内网IP+端口):127.0.0.1 127.0.0.1:10008 192.168.1.100:8080 10.0.0.5:3000 [::1]:8080
安全提示(RFC 9700):回调地址白名单采用精确匹配策略,不再支持通配符域名(如*.example.com)。每个回调地址必须完整注册,匹配时同时验证 scheme、host、port 和 path。
内网测试支持:系统完全支持内网IP+端口的白名单配置。如果您的应用部署在本地开发环境或内网服务器(如192.168.1.100:8080、10.0.0.5:3000),请将IP地址填写在IP白名单输入框中。仅企业用户支持白名单配置。
3. 集成SDK或直接调用API
您可以使用我们提供的代码示例,或者直接调用REST API进行集成。
授权端点
引导用户进行授权
这个端点就是金牛ID的登录页面。你只需要把用户引导到这个地址(拼好URL参数),用户在这里登录并同意授权后,金牛ID会自动跳回你的网站。
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| client_id | string | 是 | 应用的唯一标识(API Key) |
| redirect_uri | string | 是 | 授权成功后的回调地址,必须与白名单精确匹配 |
| response_type | string | 是 | 固定值:code |
| state | string | 是 | 用于防止CSRF攻击的随机字符串,必须为不可预测的随机值。服务端将拒绝不含state的请求。 |
| scope | string | 否 | 申请的权限范围,空格分隔。可选值:basic、profile、idcard、enterprise、qualification。默认:basic。详见Scope授权范围 |
| code_challenge | string | 否 | PKCE验证码(RFC 7636)。强烈建议Web应用和公开客户端使用。由code_verifier经S256哈希生成。详见PKCE安全指南 |
| code_challenge_method | string | 否 | PKCE哈希方法,固定值:S256。当提供code_challenge时必须同时提供此参数。 |
安全要求(RFC 9700):
state参数为必填,用于防止CSRF攻击。请使用加密安全的随机数生成器生成不少于16字节的随机字符串。- 强烈建议使用PKCE(
code_challenge+code_verifier)防止授权码注入攻击。 - 回调地址采用精确匹配,不再支持通配符域名。
请求示例
GET /oauth/authorize?response_type=code&client_id=YOUR_API_KEY&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&state=RANDOM_STATE_STRING&scope=basic%20profile%20idcard&code_challenge=CODE_CHALLENGE_VALUE&code_challenge_method=S256 HTTP/1.1 Host: id.jnqj.net
响应说明
授权成功后,将重定向到回调地址,并携带以下参数:
| 参数名 | 说明 |
|---|---|
| code | 授权码,有效期10分钟 |
| state | 原样返回请求中的state参数 |
令牌端点
使用授权码换取访问令牌(仅接受POST请求)
这一步是用"临时通行证"(code) 换 "门禁卡"(access_token)。注意:这个请求必须从你的后端服务器发出(因为需要API Secret),绝不能从前端JavaScript直接调用,否则你的密钥会暴露给用户。
安全要求:
action=token必须通过 URL 参数传递,不能放在 POST 数据中。正确的请求地址:/api/oauth.php?action=token- 此端点仅接受POST请求,GET请求将被拒绝(返回405 Method Not Allowed)
redirect_uri必须与授权请求中使用的回调地址完全一致,否则令牌交换将被拒绝- 所有出站请求必须启用SSL证书验证(
CURLOPT_SSL_VERIFYPEER=true)
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | string | 是 | 固定值:token(通过URL参数传递) |
| grant_type | string | 是 | 授权类型:authorization_code(授权码换令牌)或refresh_token(刷新令牌) |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
| code | string | 是* | 授权码。*当 grant_type=authorization_code 时必填。授权码有效期10分钟,单次使用。 |
| redirect_uri | string | 是* | 回调地址(必须与授权时一致)。*当 grant_type=authorization_code 时必填。 |
| code_verifier | string | 否* | PKCE验证器(RFC 7636)。*当授权请求中包含了code_challenge时必填,用于验证客户端身份。详见PKCE安全指南 |
| refresh_token | string | 否* | 刷新令牌。*当 grant_type=refresh_token 时必填。 |
授权码安全策略:
- 授权码有效期为10分钟,过期后无法使用
- 授权码为单次使用,使用后立即失效。若检测到授权码被重用,系统将自动撤销该客户端关联的所有令牌(RFC 9700 安全要求)
- 若授权请求使用了PKCE,令牌交换时必须提供
code_verifier,否则请求将被拒绝
响应示例(RFC 6749 扁平格式)
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "basic profile idcard"
}
响应格式说明(RFC 6749 §4.1.3):令牌端点采用扁平响应格式,access_token等字段直接位于顶层,不嵌套在data中。这与OAuth 2.0标准客户端库兼容。
响应字段说明
| 字段名 | 类型 | 说明 |
|---|---|---|
| access_token | string | 访问令牌,有效期2小时 |
| token_type | string | 令牌类型,固定值:Bearer |
| expires_in | int | 令牌有效期(秒),固定值:7200(2小时) |
| refresh_token | string | 刷新令牌,有效期30天。每次刷新会轮换生成新的refresh_token,旧令牌立即失效。 |
| scope | string | 授予的权限范围(空格分隔) |
用户信息端点
获取授权用户的基本信息(先验证令牌有效性,再检查撤销状态)
这一步是刷卡进入获取用户资料。把 access_token 放在 HTTP Header 里发给金牛ID,金牛ID验证令牌有效后返回当前登录用户的详细信息(邮箱、姓名、认证状态等)。
安全要求:
action=userinfo必须通过 URL 参数传递- 必须在 HTTP Header 中传入 Authorization,格式为
Authorization: Bearer {access_token} - 不支持通过 POST 参数传递 token,否则会返回 "缺少访问令牌" 错误
- 服务端先验证令牌签名和有效期,再检查令牌是否已被撤销(双重验证,确保安全性)
- 若令牌已被撤销,将返回
invalid_token错误(401状态码)
Scope 过滤机制(PIPL 最小必要原则):用户信息端点会根据令牌的scope自动过滤返回字段。例如,basicscope 仅返回user_id、openid、email等基本信息;idcardscope 才会返回真实姓名、身份证号等敏感字段。详见Scope授权范围。
请求头
| 参数名 | 说明 |
|---|---|
| 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对应的用户不存在 |
资质信息端点
获取用户完整资质信息(包含身份证正反面、营业执照等图片)
这个接口比 userinfo 更全面,会返回用户的身份证正反面照片URL、营业执照照片URL等完整资质信息。但是需要qualificationscope 权限才能调用,并且图片URL是HMAC签名URL(5分钟有效),不能直接保存下来以后用。
重要更新:此接口于2026年5月10日新增,专门为金牛钱包等需要获取用户完整资质信息的第三方应用提供服务。支持个人用户和企业用户两种模式,根据用户类型自动返回相应的信息。
安全要求(RFC 9700 §4.2/4.3):
- 资质端点仅接受 Authorization Header传递 access_token,已移除 GET 参数和 POST 参数传递方式(防止令牌通过浏览器历史、服务器日志、Referer头泄露)
- 访问此端点需要
qualificationscope 权限。若令牌无此权限,将返回insufficient_scope错误 - 返回的图片URL为HMAC签名URL,有效期5分钟,过期后需重新获取。详见签名URL机制
请求方式
仅支持通过 Authorization Header 传递访问令牌:
Authorization: Bearer {access_token}
已弃用的传参方式:GET 参数(?access_token=)和 POST 参数(access_token=)方式已被移除,使用这些方式将被拒绝并返回401错误。
响应数据结构
接口会根据用户类型(user_type字段)自动返回相应的信息。
个人用户响应字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| openid | string | 用户OpenID |
| user_id | int | 用户ID |
| string | 用户邮箱 | |
| username | string | 用户名 |
| user_type | string | 用户类型:personal |
| real_name | string | 真实姓名 |
| gender | string | 性别:男/女/未知 |
| nation | string | 民族 |
| birth_date | string | 出生日期(YYYY-MM-DD) |
| address | string | 详细地址 |
| city | string | 所在城市 |
| idcard_number | string | 身份证号码(脱敏显示) |
| idcard_valid_from | string | 身份证有效期开始(YYYY-MM-DD) |
| idcard_valid_to | string | 身份证有效期结束(YYYY-MM-DD) |
| idcard_front_image | string | 身份证正面照片URL(HMAC签名URL,有效期5分钟,通过/api/secure_image.php代理访问) |
| idcard_back_image | string | 身份证反面照片URL(HMAC签名URL,有效期5分钟) |
企业用户响应字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| openid | string | 用户OpenID |
| user_id | int | 用户ID |
| string | 用户邮箱 | |
| username | string | 用户名 |
| user_type | string | 用户类型:enterprise |
| enterprise | object | 企业信息对象 |
| legal_person | object | 法人信息对象 |
企业信息对象(enterprise)
| 字段名 | 类型 | 说明 |
|---|---|---|
| enterprise_name | string | 企业名称 |
| enterprise_code | string | 统一社会信用代码 |
| enterprise_type | string | 企业类型 |
| registered_capital | string | 注册资本 |
| establishment_date | string | 成立日期 |
| registered_address | string | 注册地址 |
| business_scope | string | 经营范围 |
| verified_at | string | 认证时间 |
| business_license_image | string | 营业执照照片URL(HMAC签名URL,有效期5分钟) |
法人信息对象(legal_person)
| 字段名 | 类型 | 说明 |
|---|---|---|
| legal_person_name | string | 法人姓名 |
| legal_person_idcard | string | 法人身份证号码(脱敏显示) |
| legal_person_idcard_front | string | 法人身份证正面照片URL(HMAC签名URL,有效期5分钟) |
| legal_person_idcard_back | string | 法人身份证反面照片URL(HMAC签名URL,有效期5分钟) |
响应示例 - 个人用户
{
"code": 200,
"message": "获取个人资质信息成功",
"data": {
"openid": "jn_openid_abc123def456",
"user_id": 32,
"email": "user@example.com",
"username": "金牛用户",
"user_type": "personal",
"real_name": "黎汉杰",
"gender": "男",
"nation": "汉",
"birth_date": "1983-11-29",
"address": "广东省江门市蓬江区建设路15号606",
"city": "江门市",
"idcard_number": "**************5113",
"idcard_valid_from": "2017-10-26",
"idcard_valid_to": "2037-10-26",
"idcard_front_image": "https://id.jnqj.net/uploads/idcard/jn_openid_abc123def456/20260508_lYUGcaGwq4Q2zy09.jpg",
"idcard_back_image": "https://id.jnqj.net/uploads/idcard/jn_openid_abc123def456/20260508_2wyutHT78C6cfCmn.jpg"
}
}
响应示例 - 企业用户
{
"code": 200,
"message": "获取企业资质信息成功",
"data": {
"openid": "jn_openid_xyz7890123",
"user_id": 6,
"email": "enterprise@example.com",
"username": "金牛企业",
"user_type": "enterprise",
"enterprise": {
"enterprise_name": "金牛奇迹科技有限公司",
"enterprise_code": "91440300XXXXXXXXXX",
"enterprise_type": "有限责任公司",
"registered_capital": "1000万元",
"establishment_date": "2020-01-01",
"registered_address": "广东省深圳市南山区科技园",
"business_scope": "软件开发、技术服务、技术咨询等",
"verified_at": "2024-01-15 10:30:00",
"business_license_image": "https://id.jnqj.net/uploads/enterprise/jn_openid_xyz7890123/license.jpg"
},
"legal_person": {
"legal_person_name": "张三",
"legal_person_idcard": "**************8888",
"legal_person_idcard_front": "https://id.jnqj.net/uploads/enterprise/jn_openid_xyz7890123/legal_front.jpg",
"legal_person_idcard_back": "https://id.jnqj.net/uploads/enterprise/jn_openid_xyz7890123/legal_back.jpg"
}
}
}
错误状态码说明
| HTTP状态码 | 错误码 | 错误信息 | 说明 |
|---|---|---|---|
| 401 | MISSING_ACCESS_TOKEN | 缺少访问令牌 | 未提供 access_token |
| 401 | INVALID_ACCESS_TOKEN | 无效的访问令牌 | token无效或已过期 |
| 403 | ACCOUNT_DISABLED | 账号已被禁用 | 用户账号被管理员禁用 |
| 403 | ACCOUNT_LOCKED | 账号已锁定,请X分钟后重试 | 用户因多次登录失败被临时锁定 |
| 403 | NOT_VERIFIED | 用户未完成实名认证 | 请先完成实名认证 |
| 403 | ENTERPRISE_NOT_VERIFIED | 企业用户未完成企业认证 | 企业用户请先完成企业认证 |
| 403 | IDCARD_EXPIRED | 身份证已过期 | 请重新认证身份证 |
| 404 | USER_NOT_FOUND | 用户不存在 | token对应的用户不存在 |
| 404 | IDCARD_NOT_FOUND | 未找到身份证认证信息 | 未找到有效的身份证认证信息 |
| 500 | SYSTEM_ERROR | 系统错误 | 系统内部错误,请稍后重试 |
安全提示:
- 图片URL可能包含访问时间限制,请在获取后及时保存或下载
- 建议将图片缓存到您的服务器,避免频繁请求我们的接口
- 请妥善保管用户资质信息,不要泄露给未授权的第三方
刷新令牌
访问令牌有效期为2小时,过期后可以使用刷新令牌获取新的访问令牌。
access_token 就像临时停车券,2小时后过期。refresh_token 就像停车场的月卡,30天内可以反复用它换新的停车券。每次刷新后,会给你一张新的月卡(旧的作废),如果系统发现有人用了已经作废的月卡,会立刻把所有卡都注销(防盗机制)。
刷新访问令牌
action=token必须通过 URL 参数传递,与获取令牌使用相同的端点。
刷新令牌安全策略(RFC 9700 §4.14):
- 有效期:刷新令牌有效期为30天,过期后需用户重新授权
- 令牌轮换:每次刷新令牌时,系统会生成全新的 refresh_token,旧的 refresh_token 立即失效。客户端必须使用每次刷新响应中返回的新 refresh_token
- 重用检测:若已失效的旧 refresh_token 被再次使用,系统将检测到重用行为,并自动撤销该令牌族的所有令牌(包括所有关联的 access_token 和 refresh_token),以防止令牌被盗后长期使用
- 令牌族绑定:refresh_token 绑定到特定的 client_id,防止跨客户端重用
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | string | 是 | 固定值:token(通过URL参数传递) |
| grant_type | string | 是 | 固定值:refresh_token |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
| refresh_token | string | 是 | 刷新令牌(使用最近一次刷新返回的新令牌) |
响应示例(RFC 6749 扁平格式)
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "NEW_REFRESH_TOKEN_VALUE...",
"scope": "basic profile idcard"
}
重要:每次刷新后,必须使用响应中返回的新 refresh_token替换旧的。若使用已失效的旧 refresh_token,将触发重用检测,导致整个令牌族被撤销。
扫码登录
扫码登录是一种便捷的第三方登录方式,用户只需使用金牛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. 查询二维码状态
查询二维码当前状态
安全要求(重要):
client_secret必须通过POST请求体或Authorization请求头(Basic Auth)传输- 禁止通过 GET URL 参数传输
client_secret(如?client_secret=xxx),否则请求将被拒绝 - GET参数传输密钥会导致密钥泄露到浏览器历史、服务器日志、Referer头中
- 所有出站请求必须启用SSL证书验证
client_secret 安全传输方式:
- 方式一(推荐):POST请求体传递
client_secret=YOUR_SECRET - 方式二:HTTP Basic Auth头
Authorization: Basic base64(client_id:client_secret)
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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平台实施了多层安全防护机制,覆盖从请求接入到令牌管理的完整生命周期。以下章节逐一说明各项安全措施,帮助开发者快速理解并正确配置。
金牛ID 安全防护体系
请求安全
CSRF防护
HTTPS强制
SSL证书验证
身份安全
密码策略
回调白名单
速率限制
令牌安全
安全随机数
令牌轮换
重用检测
安全防护就像银行的多重安保:进门要刷门禁卡(CSRF令牌),密码要够复杂(密码策略),只能去你登记过的柜台办理业务(回调白名单),所有通道都是加密的(HTTPS),还有摄像头监控异常行为(速率限制+审计日志)。
CSRF防护(跨站请求伪造防护)
金牛ID对所有POST请求实施CSRF令牌验证机制,防止恶意网站伪造用户请求。
CSRF令牌使用方式
所有POST请求必须携带CSRF令牌,支持以下两种方式传递:
| 传递方式 | 字段名 | 示例 | 适用场景 |
|---|---|---|---|
| HTTP请求头 | X-CSRF-Token |
X-CSRF-Token: your_csrf_token_here |
API调用、AJAX请求 |
| POST请求体 | csrf_token |
csrf_token=your_csrf_token_here |
Web表单提交 |
安全要求:
- 所有POST请求(登录、注册、密码修改、令牌撤销等)必须携带CSRF令牌,否则请求将被拒绝(返回403错误)
- GET请求不需要CSRF令牌(如授权端点跳转、用户信息查询)
- CSRF令牌通过用户Session生成,每次登录会生成新的令牌
- OAuth API调用(使用Bearer Token认证的请求)不需要CSRF令牌,因为已通过Bearer Token验证身份
代码示例:在表单中添加CSRF令牌
<?php
// PHP:在表单中嵌入CSRF令牌session_start();
$csrfToken = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrfToken;
?>
<!-- HTML表单中添加隐藏字段 -->
<form method="POST" action="/api/login.php">
<input type="hidden" name="csrf_token" value="<?php echo $csrfToken; ?>">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
<!-- AJAX请求中通过Header传递 -->
<script>
fetch('/api/login.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': '<?php echo $csrfToken; ?>'
},
body: 'username=user&password=pass'
});
</script>CSRF令牌就像快递取件码:即使有人冒充你去寄件,没有取件码也发不了。每次登录时系统给你一个随机取件码,你提交表单时必须带上这个码,系统验证正确后才处理请求。
密码安全策略
金牛ID对用户密码实施强制安全策略,确保密码强度满足安全要求。
密码强度要求
| 规则 | 要求 | 说明 |
|---|---|---|
| 最小长度 | 至少8位 | 密码长度不足8位将被拒绝 |
| 大小写字母 | 必须包含大写和小写字母 | 如A和a至少各一个 |
| 数字 | 必须包含数字 | 至少一个数字字符(0-9) |
| 弱密码检测 | 系统会检测常见弱密码 | 如12345678、password等将被拒绝 |
适用场景:密码策略在注册和修改密码时强制执行。示例合格密码:Abc12345、MyP@ss2024、Jnqj2026。
密码策略就像开锁的钥匙要求:钥匙不能太短(至少8齿),不能全是平齿(要有大小写+数字),这样才不容易被撬开。
回调地址白名单与HTTPS强制
金牛ID对回调地址实施严格的安全管控,确保授权回调不会被劫持到恶意网站。
白名单强制要求
| 安全规则 | 说明 |
|---|---|
| 白名单必填 | 使用OAuth前必须配置redirect_uri白名单。空白名单将拒绝所有回调请求,授权流程无法完成 |
| 精确匹配 | 回调地址采用精确匹配(scheme + host + port + path),不支持通配符 |
| HTTPS强制 | 生产环境回调地址必须使用HTTPS协议,HTTP将被拒绝 |
| 开发环境例外 | 仅localhost和127.0.0.1允许使用HTTP(本地开发调试用) |
重要提醒:如果您在白名单中配置了www.example.com,但回调地址使用的是http://www.example.com/callback.php(HTTP协议),授权将被拒绝。请确保生产环境回调地址始终使用https://开头。
回调白名单就像快递收件地址白名单:你必须提前登记好收件地址(白名单),快递(授权码)只会送到你登记过的地址。而且生产环境必须用加密通道(HTTPS)运输,防止包裹被半路截走。本地开发可以用普通通道(HTTP + localhost)。
令牌安全机制
金牛ID对Access Token和Refresh Token实施多重安全保护,符合OAuth 2.0安全最佳实践(RFC 9700)。
Access Token安全
| 安全特性 | 说明 |
|---|---|
| 安全随机生成 | 使用256位密码学安全随机数生成(CSPRNG),不可预测、不可枚举 |
| 有效期限制 | 有效期2小时,过期后必须刷新或重新授权 |
| 撤销机制 | 支持通过撤销端点主动撤销,撤销后立即失效 |
| 存储位置 | 建议存储在HttpOnly Cookie或服务器端Session中,不要存储在localStorage |
Refresh Token安全
| 安全特性 | 说明 |
|---|---|
| 令牌轮换 | 每次刷新时生成全新的refresh_token,旧令牌立即失效 |
| 重用检测 | 若已失效的旧refresh_token被再次使用,系统将自动撤销该令牌族的所有令牌(防盗机制) |
| 令牌族绑定 | refresh_token绑定到特定client_id,防止跨客户端重用 |
| 有效期 | 有效期30天,过期后需用户重新授权 |
令牌安全就像酒店房卡系统:Access Token是房卡(2小时有效),Refresh Token是续卡凭证(30天有效)。每次续卡时给你一张新卡(轮换),旧卡立刻作废。如果有人拿你已经作废的旧卡去续卡(重用),系统会发现异常,把你所有的卡全部注销(重用检测),逼你重新去前台登记(重新授权)。
安全图片访问
身份证、营业执照等敏感图片采用多层安全保护,确保用户隐私数据不被未授权访问。
安全图片访问机制
| 安全层 | 机制 | 说明 |
|---|---|---|
| 认证层 | 登录认证 | 必须携带有效的Access Token才能访问图片URL |
| 签名层 | HMAC-SHA256签名 | 图片URL附带HMAC签名,篡改URL参数将导致签名验证失败 |
| 时效层 | 5分钟有效期 | 签名URL有效期仅5分钟,过期后需重新获取 |
| 防护层 | 目录直接访问禁止 | /uploads/idcard/和/uploads/enterprise/目录直接访问返回403 |
开发者注意:
- 图片URL通过
/api/secure_image.php代理端点访问,不能直接拼接路径访问 - 签名URL有效期仅5分钟,请及时使用,不要缓存或长期存储URL
- 所有图片访问请求都会被记录审计日志
- 建议将图片下载到您的服务器缓存,避免频繁请求
安全图片访问就像银行保险箱:首先你要是银行客户(登录认证),然后银行给你一张限时5分钟的授权条(HMAC签名URL),你拿着授权条去保险箱取东西。过了5分钟授权条失效,就得重新找银行要。而且保险箱的钥匙孔(目录直接访问)是封死的,只能通过银行柜台(代理端点)取东西。
SSL证书验证与安全响应头
金牛ID对所有出站请求启用SSL证书验证,并设置了完整的安全响应头,防范中间人攻击和各类Web安全威胁。
SSL证书验证
开发者必须注意:在调用金牛ID API时,必须启用SSL证书验证,不要关闭。以下是各语言正确配置:
// PHP cURL - 必须启用SSL验证$ch = curl_init('https://id.jnqj.net/api/oauth.php?action=token');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 验证对端证书(必须为true)curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 验证主机名(必须为2)// 不要设置为 false 或 0,否则存在中间人攻击风险!
// Python requests - 默认已启用SSL验证,不要关闭response = requests.post(url, data=data) # 正确:默认verify=True# 不要使用 verify=False,否则存在安全风险!
// Node.js axios - 默认已启用SSL验证const response = await axios.post(url, data); // 正确:默认验证SSL// 不要设置 rejectUnauthorized: false!
// Java HttpURLConnection - 默认已启用SSL验证// 使用默认的HttpsURLConnection即可,无需额外配置安全响应头
金牛ID服务器返回以下安全响应头,保护用户浏览器免受常见Web攻击:
| 响应头 | 作用 | 说明 |
|---|---|---|
Strict-Transport-Security |
HSTS | 强制浏览器使用HTTPS连接,防止SSL降级攻击 |
Content-Security-Policy |
CSP | 内容安全策略,防止XSS攻击和数据注入 |
X-Frame-Options |
点击劫持防护 | 防止页面被嵌入iframe,防止点击劫持攻击 |
X-Content-Type-Options |
MIME类型防护 | 禁止浏览器猜测MIME类型,防止内容类型嗅探 |
Referrer-Policy |
Referrer控制 | 控制Referrer信息泄露,保护用户隐私 |
Permissions-Policy |
权限策略 | 限制浏览器API访问权限(如摄像头、麦克风等) |
开发者提示:以上安全响应头由金牛ID服务器自动设置,您无需在客户端额外配置。但如果您在自己的应用中代理金牛ID的响应,请确保不剥离这些安全头。
Scope 授权范围机制
金牛ID实现了基于OAuth 2.0的scope授权范围机制,遵循 PIPL(个人信息保护法)最小必要原则和单独同意原则。第三方应用通过scope参数申请所需的权限范围,系统据此限制令牌可访问的字段,确保应用只能获取其业务所需的信息。
可用Scope列表
| Scope | 权限说明 | 可访问的字段 |
|---|---|---|
basic |
基本信息(默认) | user_id, openid, email, username, user_type, real_name_verified |
profile |
个人资料 | nickname, avatar, phone(依赖basic) |
idcard |
身份证信息(敏感) | real_name, gender, birth_date, nation, city, address, idcard_number, idcard_issue_authority, idcard_valid_period(依赖basic) |
enterprise |
企业信息 | enterprise对象(企业名称、统一社会信用代码等,依赖basic) |
qualification |
资质照片(最高权限) | idcard_front_image, idcard_back_image, business_license_image, legal_person_idcard_front/back(依赖basic,需单独申请) |
Scope使用规则:
- scope参数在授权端点请求时传入,多个scope用空格分隔
- 若不传scope参数,默认授予
basic权限 - scope具有依赖关系:申请
qualification会自动包含basic权限 - 用户信息端点会根据令牌的scope自动过滤返回字段
- 资质信息端点要求
qualificationscope,否则返回insufficient_scope错误
请求示例
GET /oauth/authorize?response_type=code
&client_id=YOUR_API_KEY
&redirect_uri=https://app.example.com/callback
&state=RANDOM_STATE
&scope=basic%20profile%20idcard%20qualification
PKCE 安全指南(RFC 7636)
PKCE(Proof Key for Code Exchange)是OAuth 2.0的安全扩展,用于防止授权码注入攻击。RFC 9700(OAuth 2.0 Security BCP)要求所有授权服务器必须支持PKCE。金牛ID已全面支持PKCE,强烈建议所有第三方应用启用。
PKCE工作流程
- 客户端生成一个高熵随机字符串
code_verifier(43-128字符) - 客户端计算
code_challenge= BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) - 客户端在授权请求中发送
code_challenge和code_challenge_method=S256 - 授权服务器存储
code_challenge并返回授权码 - 客户端在令牌交换请求中发送
code_verifier - 授权服务器验证 SHA256(code_verifier) 是否匹配存储的 code_challenge
各语言PKCE实现示例
PHP
// 1. 生成 code_verifier$codeVerifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// 2. 生成 code_challenge (S256)$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
// 3. 构建授权URL$authUrl = 'https://id.jnqj.net/oauth/authorize?response_type=code'
. '&client_id=' . urlencode($apiKey)
. '&redirect_uri=' . urlencode($callbackUrl)
. '&state=' . urlencode($state)
. '&code_challenge=' . $codeChallenge
. '&code_challenge_method=S256';
// 4. 令牌交换时传入 code_verifier$tokenParams = [
'grant_type' => 'authorization_code',
'client_id' => $apiKey,
'client_secret' => $apiSecret,
'code' => $authCode,
'redirect_uri' => $callbackUrl,
'code_verifier' => $codeVerifier, // PKCE验证器];
JavaScript / Node.js
const crypto = require('crypto');
// 1. 生成 code_verifierconst codeVerifier = crypto.randomBytes(32).toString('base64url');
// 2. 生成 code_challenge (S256)const codeChallenge = crypto.createHash('sha256')
.update(codeVerifier).digest('base64url');
// 3. 构建授权URLconst authUrl = `https://id.jnqj.net/oauth/authorize?response_type=code`
+ `&client_id=${encodeURIComponent(apiKey)}`
+ `&redirect_uri=${encodeURIComponent(callbackUrl)}`
+ `&state=${encodeURIComponent(state)}`
+ `&code_challenge=${codeChallenge}`
+ `&code_challenge_method=S256`;
// 4. 令牌交换时传入 code_verifierconst tokenParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: apiKey,
client_secret: apiSecret,
code: authCode,
redirect_uri: callbackUrl,
code_verifier: codeVerifier, // PKCE验证器});
PKCE兼容性说明:PKCE为可选项(向后兼容)。若授权请求中未包含code_challenge,令牌交换时也不需要code_verifier。但出于安全考虑,强烈建议所有应用使用PKCE。
签名URL机制
资质信息端点返回的身份证照片、营业执照等敏感图片URL采用HMAC-SHA256签名机制,替代了此前"可直接访问"的方式。此机制遵循 PIPL 第51条数据安全保护义务。
签名URL特性
| 特性 | 说明 |
|---|---|
| 有效期 | 5分钟,过期后URL失效,需重新调用资质端点获取新URL |
| 签名算法 | HMAC-SHA256 |
| 访问代理 | 所有图片通过/api/secure_image.php代理端点访问 |
| 路径保护 | URL路径中不暴露用户OpenID,使用随机nonce防止猜测 |
| 防缓存 | 响应头设置Cache-Control: no-store和Referrer-Policy: no-referrer |
| 直接访问拦截 | /uploads/idcard/和/uploads/enterprise/目录的直接访问已被Web服务器禁止(返回403) |
签名URL格式
https://id.jnqj.net/api/secure_image.php?path=BASE64_ENCODED_PATH&expires=TIMESTAMP&nonce=RANDOM_HEX&uid=USER_ID&sig=HMAC_SIGNATURE
使用注意:
- 签名URL有效期仅5分钟,请及时使用,不要缓存或存储URL
- 若URL过期,请重新调用资质信息端点获取新的签名URL
- 签名URL中包含用户ID和签名信息,请勿泄露给第三方
- 所有图片访问请求都会被记录审计日志
速率限制
为防止暴力枚举和滥用,金牛ID对所有API端点实施了IP级速率限制。超过限制时返回 HTTP 429 状态码和slow_down错误。
| 端点 | 限制 | 标识 |
|---|---|---|
| 登录端点(login) | 20次/5分钟 | IP |
| 注册端点(register) | 5次/小时 | IP |
| 密码修改端点(password change) | 10次/10分钟 | IP + user_id |
| 令牌端点(token) | 10次/分钟 | IP + client_id |
| 用户信息端点(userinfo) | 60次/分钟 | IP |
| 资质信息端点(qualification) | 30次/分钟 | IP |
| 扫码状态查询(qrlogin status) | 30次/分钟 | IP |
速率限制说明:
- 登录端点:每个IP每5分钟最多尝试登录20次,超过将返回429错误
- 注册端点:每个IP每小时最多注册5次,防止批量注册
- 密码修改端点:每个用户每10分钟最多修改密码10次
- OAuth令牌端点:每个IP+client_id组合每分钟最多请求10次
- 触发速率限制后,请等待对应时间窗口后再重试
限流响应
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"success": false,
"error": "slow_down",
"error_description": "请求过于频繁,请稍后再试"
}
令牌撤销端点(RFC 7009)
撤销访问令牌或刷新令牌(使用客户端凭证认证)
令牌撤销端点允许客户端主动通知授权服务器撤销先前获取的令牌,用于处理用户登出、令牌泄露等场景。
安全要求:
- 撤销端点使用客户端凭证认证(client_id + client_secret),确保只有令牌的合法所有者才能撤销它
- 必须通过 POST 请求体传递
client_id和client_secret参数进行身份验证 - 服务端会验证请求者是否为令牌的原始颁发对象,防止恶意第三方撤销他人令牌
- 所有出站请求必须启用SSL证书验证
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| token | string | 是 | 要撤销的令牌(access_token 或 refresh_token) |
| token_type_hint | string | 否 | 令牌类型提示:access_token或refresh_token |
| client_id | string | 是 | API Key |
| client_secret | string | 是 | API Secret |
响应说明
无论令牌是否存在,服务端都返回 HTTP 200 和空JSON体{}(RFC 7009 §2.2,不通过错误码泄露令牌是否存在)。
撤销行为:
- 撤销 access_token 后,该令牌立即失效,无法再用于访问用户信息和资质端点
- 撤销 refresh_token 后,该刷新令牌无法再用于获取新的 access_token
- 所有撤销操作都会被记录到审计日志
令牌内省端点(RFC 7662)
查询令牌的有效性和权限信息(验证令牌归属)
令牌内省端点允许资源服务器查询令牌的有效性、关联的用户和权限范围。由于金牛ID的 access_token 为不透明令牌(非JWT),第三方应用需通过内省端点验证令牌。
安全要求:
- 内省端点会验证令牌归属:只有令牌的原始颁发对象(通过client_id + client_secret认证)才能查询该令牌的状态
- 防止恶意客户端探测其他应用的令牌状态
- 如果令牌不属于当前认证的客户端,将返回
{"active": false}(不泄露令牌是否存在) - 所有出站请求必须启用SSL证书验证
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| token | string | 是 | 要查询的令牌 |
| client_id | string | 是 | API Key(内省需要客户端认证) |
| client_secret | string | 是 | API Secret |
响应示例(有效令牌)
{
"active": true,
"scope": "basic profile idcard",
"client_id": "jn_xxxxxxxxxxxx",
"username": "jenico",
"token_type": "Bearer",
"exp": 1782698000,
"iat": 1782690800,
"sub": "12345"
}
响应示例(无效/过期/已撤销令牌)
{
"active": false
}
发现端点(RFC 8414)
金牛ID提供OAuth 2.0授权服务器元数据发现端点,允许客户端自动获取端点地址、支持的授权类型、签名密钥等配置信息。
授权服务器元数据
获取授权服务器元数据
{
"issuer": "https://id.jnqj.net",
"authorization_endpoint": "https://id.jnqj.net/oauth/authorize",
"token_endpoint": "https://id.jnqj.net/api/oauth.php?action=token",
"revocation_endpoint": "https://id.jnqj.net/api/oauth.php?action=revocation",
"introspection_endpoint": "https://id.jnqj.net/api/oauth.php?action=introspect",
"jwks_uri": "https://id.jnqj.net/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"scopes_supported": ["basic", "profile", "idcard", "enterprise", "qualification"],
"code_challenge_methods_supported": ["S256", "plain"],
"token_types_supported": ["Bearer"]
}
JWKS端点
获取JSON Web Key Set(当前为空,令牌为不透明令牌)
当前金牛ID的 access_token 为不透明令牌(非JWT),客户端需通过令牌内省端点验证令牌有效性。JWKS端点为未来JWT令牌支持预留。
错误码参考
金牛ID API采用RFC 6749 §5.2标准错误响应格式。
标准错误响应格式
{
"success": false,
"error": "invalid_request",
"error_description": "缺少必要的参数"
}
错误码列表
| error | HTTP状态码 | 说明 |
|---|---|---|
invalid_request |
400 | 请求缺少必要参数或参数格式错误 |
invalid_client |
401 | 客户端认证失败(API Key或Secret错误) |
invalid_grant |
400/401 | 授权码无效/过期/已使用,或刷新令牌无效/过期/重用 |
invalid_token |
401 | 访问令牌无效、过期或已被撤销 |
invalid_scope |
400 | 请求的scope包含不支持的值 |
insufficient_scope |
403 | 令牌的scope权限不足,无法访问该资源 |
slow_down |
429 | 请求频率超过速率限制 |
server_error |
500 | 服务器内部错误 |
授权端点错误回调
| error | 说明 |
|---|---|
invalid_client |
client_id无效或未找到 |
invalid_redirect_uri |
回调地址不在白名单中 |
real_name_required |
用户未完成实名认证 |
idcard_expired |
用户身份证已过期 |
access_denied |
用户拒绝授权 |
安全提示:错误回调中不再包含user_type字段。如需区分用户类型,请通过用户信息端点获取。
安全最佳实践
以下是金牛ID为第三方应用开发者提供的安全最佳实践建议,帮助您在接入过程中最大程度地保护用户数据和系统安全。
1. 密钥存储建议
核心原则:API Secret 是您的应用密码,必须像保护银行卡密码一样保护它。
| 建议项 | 正确做法 | 错误做法(危险) |
|---|---|---|
| 存储方式 | 使用环境变量($_ENV或getenv()) |
硬编码在代码中 |
| 配置文件 | 使用.env文件并加入.gitignore |
将配置文件提交到版本控制 |
| 密钥管理 | 使用专业密钥管理服务(如Vault、KMS) | 存储在数据库明文字段中 |
| 访问控制 | 限制配置文件的文件系统权限 | 所有用户可读的配置文件 |
| 前端暴露 | 仅在服务器端使用 | 放在JavaScript、HTML、移动APP中 |
// PHP 正确的密钥存储方式// .env 文件内容:// JNQJ_API_KEY=jn_your_api_key_here
// JNQJ_API_SECRET=sk_your_api_secret_here
// PHP 代码中读取:$apiKey = getenv('JNQJ_API_KEY');
$apiSecret = getenv('JNQJ_API_SECRET');
// .gitignore 文件中加入:// .env2. Token 存储建议
核心原则:Access Token 和 Refresh Token 都是敏感凭证,必须安全存储,防止被窃取。
| 存储方式 | 安全等级 | 说明 |
|---|---|---|
| HttpOnly Cookie | 推荐 | 设置HttpOnly、Secure、SameSite=Strict属性,JavaScript无法读取 |
| 服务器端Session | 推荐 | 将Token存储在服务端Session中,客户端只持有Session ID |
| 加密存储 | 可用 | 使用AES-256等算法加密后存储在数据库中 |
| localStorage | 不推荐 | JavaScript可直接读取,存在XSS攻击风险 |
| URL参数 | 禁止 | 会泄露到浏览器历史、服务器日志、Referer头 |
// PHP 正确的Token存储方式(HttpOnly Cookie)session_start();
// 获取令牌后,存储到Session中$_SESSION['access_token'] = $tokenData['access_token'];
$_SESSION['refresh_token'] = $tokenData['refresh_token'];
// 或者使用HttpOnly Cookie(注意需要HTTPS)setcookie('access_token', $tokenData['access_token'], [
'expires' => time() + 7200, // 2小时有效期 'path' => '/',
'secure' => true, // 仅HTTPS传输 'httponly' => true, // JavaScript不可读 'samesite' => 'Strict', // 防止CSRF]);3. 错误处理建议
核心原则:错误信息不应暴露系统内部细节,给用户的提示要友好且不泄露敏感信息。
| 场景 | 正确做法 | 错误做法(危险) |
|---|---|---|
| API调用失败 | 返回通用错误提示"服务暂时不可用,请稍后重试" | 直接返回API的错误堆栈或SQL语句 |
| 令牌过期 | 提示"登录已过期,请重新登录"并跳转登录 | 显示具体的Token值或过期时间戳 |
| 网络错误 | 提示"网络连接异常,请检查网络" | 显示服务器IP、端口等内部信息 |
| 权限不足 | 提示"您没有权限执行此操作" | 显示具体的scope名称或内部权限结构 |
// PHP 正确的错误处理方式try {
$tokenInfo = $oauth->getAccessToken($code, $redirectUri);
if (!isset($tokenInfo['access_token'])) {
// 记录详细错误到服务器日志(不暴露给用户) error_log('OAuth token exchange failed: ' . json_encode($tokenInfo));
// 返回友好的错误提示 throw new Exception('登录服务暂时不可用,请稍后重试');
}
} catch (Exception $e) {
// 记录详细错误到日志 error_log('OAuth error: ' . $e->getMessage());
// 向用户显示友好错误 $_SESSION['error_message'] = '登录过程中出现问题,请重试';
header('Location: /login');
exit;
}4. 日志记录建议
核心原则:日志用于安全审计和问题排查,但绝不记录敏感字段。
| 日志项 | 可以记录 | 禁止记录 |
|---|---|---|
| 请求信息 | 时间戳、IP地址、请求端点、HTTP方法 | 完整的请求体(可能含密钥) |
| 认证信息 | client_id、用户ID、认证结果(成功/失败) | API Secret、access_token、refresh_token |
| 用户数据 | 用户ID、操作类型、操作时间 | 密码、身份证号、手机号、真实姓名 |
| 错误信息 | 错误码、错误类型、调用栈 | 包含敏感数据的错误详情 |
// PHP 正确的日志记录方式// 定义需要脱敏的字段$sensitiveFields = ['password', 'client_secret', 'access_token',
'refresh_token', 'idcard_number', 'real_name'];
function safeLog($message, $context = []) {
$sensitiveFields = ['password', 'client_secret', 'access_token',
'refresh_token', 'idcard_number', 'real_name'];
// 脱敏处理 foreach ($sensitiveFields as $field) {
if (isset($context[$field])) {
$context[$field] = '***REDACTED***';
}
}
// 记录日志(不含敏感信息) error_log($message . ': ' . json_encode($context));
}
// 使用示例safeLog('OAuth login attempt', [
'client_id' => $apiKey,
'user_ip' => $_SERVER['REMOTE_ADDR'],
'result' => 'success',
// 以下字段会被自动脱敏 'password' => $userPassword, // 记录为 ***REDACTED***
]);安全最佳实践清单
密钥存储:环境变量 + .gitignore
Token存储:HttpOnly Cookie 或服务器Session
错误处理:友好提示用户 + 详细日志服务端
日志记录:记录审计信息 + 脱敏敏感字段
SSL验证:始终启用 + 永不关闭
密钥传输:POST请求体或Header + 禁止GET参数
技术架构概览
金牛 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;
}
// 生成PKCE code_verifier public function generateCodeVerifier() {
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
}
// 生成PKCE code_challenge (S256) public function generateCodeChallenge($codeVerifier) {
return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
}
// 获取授权URL(state必填,支持PKCE和scope) public function getAuthUrl($redirectUri, $state, $scope = 'basic', $codeChallenge = null) {
$params = [
'client_id' => $this->apiKey,
'redirect_uri' => $redirectUri,
'response_type' => 'code',
'state' => $state,
'scope' => $scope,
];
if ($codeChallenge) {
$params['code_challenge'] = $codeChallenge;
$params['code_challenge_method'] = 'S256';
}
return $this->baseUrl . '/oauth/authorize?' . http_build_query($params);
}
// 获取访问令牌(支持PKCE) public function getAccessToken($code, $redirectUri, $codeVerifier = null) {
$data = [
'grant_type' => 'authorization_code',
'client_id' => $this->apiKey,
'client_secret' => $this->apiSecret,
'code' => $code,
'redirect_uri' => $redirectUri,
];
if ($codeVerifier) {
$data['code_verifier'] = $codeVerifier;
}
$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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 启用SSL证书验证 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// 撤销令牌 (RFC 7009) public function revokeToken($token, $tokenTypeHint = 'access_token') {
$data = [
'token' => $token,
'token_type_hint' => $tokenTypeHint,
'client_id' => $this->apiKey,
'client_secret' => $this->apiSecret,
];
$ch = curl_init($this->baseUrl . '/api/oauth.php?action=revocation');
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); // 启用SSL证书验证 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_exec($ch);
curl_close($ch);
}
}
// 使用示例$oauth = new JnqjOAuth('YOUR_API_KEY', 'YOUR_API_SECRET');
// 1. 生成PKCE参数$codeVerifier = $oauth->generateCodeVerifier();
$codeChallenge = $oauth->generateCodeChallenge($codeVerifier);
// 2. 生成授权URL(state必填)$state = bin2hex(random_bytes(16)); // 生成随机state$authUrl = $oauth->getAuthUrl(
'https://app.example.com/callback',
$state,
'basic profile idcard',
$codeChallenge
);
// 3. 用户授权后,用授权码交换令牌$tokenInfo = $oauth->getAccessToken($authCode, 'https://app.example.com/callback', $codeVerifier);
// $tokenInfo['access_token'] - 访问令牌// $tokenInfo['refresh_token'] - 刷新令牌(每次刷新轮换)// $tokenInfo['scope'] - 授予的权限范围
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",
}
}
// 获取授权URLfunc (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
- 支持企业账号添加内网IP+端口到白名单(需审核)
通用安全建议
- 使用HTTPS协议进行所有API调用
- 妥善存储API Secret,建议使用环境变量或密钥管理服务
- 实现适当的访问令牌刷新机制
- 验证state参数以防止CSRF攻击
- 定期检查API使用日志,发现异常及时处理
- 所有cURL请求启用SSL证书验证(CURLOPT_SSL_VERIFYPEER=true, CURLOPT_SSL_VERIFYHOST=2)
- client_secret 不通过GET参数传输,使用POST请求体或Authorization头
- 了解各端点的速率限制,避免触发限流
- 敏感图片(身份证等)需通过HMAC签名URL访问,有效期仅5分钟
常见问题
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: 出于安全考虑,金牛ID回调地址采用精确匹配,不再支持通配符域名。请将每个需要使用的回调地址逐一添加到白名单中,例如app.example.com、api.example.com需分别添加。
Q: 配置白名单后多久生效?
A: 配置立即生效,无需等待。
如有其他问题,请联系客服:Jenico.Li | Jenico@vip.qq.com