金牛ID OAuth 开发文档

金牛ID提供标准的OAuth 2.0授权服务,让第三方应用可以快速接入金牛ID账号体系,实现一键登录功能。

OAuth 2.0 授权流程

金牛ID采用OAuth 2.0标准的授权码模式(Authorization Code Flow),流程如下:

  1. 第三方应用引导用户访问金牛ID授权页面
  2. 用户登录并同意授权
  3. 金牛ID重定向回第三方应用,携带授权码或错误信息
  4. 第三方应用使用授权码换取访问令牌
  5. 使用访问令牌获取用户信息

回调错误码说明

当授权过程中出现错误时,金牛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个令牌)
主要用途 个人登录、实名认证 第三方应用接入、统一身份认证
重要说明

企业用户需要先创建应用,然后为应用创建令牌才能获取 API Key。个人用户可以通过企业用户的 API Key 登录授权。

1. 注册并完成认证

首先,您需要注册一个金牛ID账号并完成实名认证。企业用户完成企业认证后,需要按照以下流程获取 API 密钥:

企业用户获取 API 密钥流程:

  1. 创建应用 - 进入应用管理页面,创建新应用(最多300个)
  2. 配置白名单 - 设置回调域名和IP白名单
  3. 创建令牌 - 为应用创建 API 令牌(每个应用限1个)
  4. 保存密钥 - 获取 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进行集成。

授权端点

GET /oauth/authorize

引导用户进行授权

请求参数

参数名 类型 必填 说明
client_id string 应用的唯一标识(API Key)
redirect_uri string 授权成功后的回调地址
response_type string 固定值:code
state string 用于防止CSRF攻击的随机字符串

响应说明

授权成功后,将重定向到回调地址,并携带以下参数:

参数名 说明
code 授权码,有效期10分钟
state 原样返回请求中的state参数

令牌端点

POST /api/oauth.php?action=token

使用授权码换取访问令牌

重要提示

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..."
    }
}

用户信息端点

GET /api/oauth.php?action=userinfo

获取授权用户的基本信息

重要提示

1. action=userinfo 必须通过 URL 参数传递
2. 必须在 HTTP Header 中传入 Authorization,格式为 Authorization: Bearer {access_token}
3. 不支持通过 POST 参数传递 token,否则会返回 "缺少访问令牌" 错误

请求头

参数名 说明
Authorization Bearer {access_token}

响应字段说明

字段名 类型 说明
user_id int 用户ID
email 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小时,过期后可以使用刷新令牌获取新的访问令牌。

POST /api/oauth.php?action=token

刷新访问令牌

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 扫描二维码即可完成授权登录,无需输入账号密码。

优势:扫码登录更加安全便捷,用户无需在第三方网站输入密码,有效防止密码泄露风险。

扫码登录流程

  1. 第三方应用调用 create 接口创建二维码
  2. 第三方应用展示二维码给用户
  3. 用户使用金牛ID App 扫描二维码
  4. 用户在手机上确认授权
  5. 第三方应用轮询 status 接口获取登录状态
  6. 用户确认后,第三方应用获取 access_token
  7. 使用 access_token 获取用户信息

扫码登录API接口

1. 创建二维码

POST /api/qrlogin.php?action=create

创建扫码登录二维码

请求参数
参数名 类型 必填 说明
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. 查询二维码状态

GET /api/qrlogin.php?action=status

查询二维码当前状态

请求参数
参数名 类型 必填 说明
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. 获取用户信息

GET /api/qrlogin.php?action=get_userinfo

使用扫码登录的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包含以下核心功能:

下载扫码登录SDK

技术架构概览

金牛 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(授权协议)

💡 比喻:临时门禁卡 —— 只管 "允许第三方做什么",不管 "用户是谁"

核心作用:安全授权,不暴露用户密码,权限可控

金牛场景流程:

  1. 合作网站 / 游戏想接入金牛登录
  2. 用户点击 "金牛 ID 登录" → 跳转到 id.jnqj.net
  3. 用户同意授权(比如 "允许获取昵称 / 头像")
  4. 金牛给第三方发临时令牌(Access Token)
  5. 第三方用令牌访问用户资源,全程看不到密码

2)OpenID Connect(OIDC,身份认证层)

💡 比喻:带防伪照片的身份证 —— 在 OAuth 2.0 基础上,明确告诉第三方 "用户是谁"

关键升级:多了 ID Token(加密 JWT),包含:

金牛场景:

3)对金牛 ID 的业务价值

价值点 说明
统一账号对外输出 所有合作方都用同一套身份标准
安全可靠 密码不流出、令牌短期有效、可随时撤销
国际标准 接入方按国际标准开发,接入快、兼容性好
单点登录(SSO) 一次登录,全金牛生态通行

二、接口:RESTful API(登录 / 授权 / 用户信息)

一句话定位:金牛 ID 对外提供的标准化 "功能调用入口",让第三方 / 内部系统能程序化调用登录、授权、获取用户信息。

1)什么是 RESTful API(大白话)

接口示例:

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 业务流程闭环

1
用户 / 第三方 → 调用 RESTful API 发起登录 / 授权
2
系统按 OAuth 2.0/OIDC 协议完成安全验证、发令牌
3
全程记录日志到 ELK / 自研系统,用于审计与监控
4
异常时自动告警、拦截,保障账号安全

🎯 一句话总结

OAuth 2.0/OIDC 定安全规则,RESTful API 做功能入口,ELK 日志 做安全兜底,
三位一体支撑金牛 ID 的统一身份与开放授权业务。

企业用户应用管理

一句话定位:企业用户通过应用管理系统,可以创建和管理多个应用,每个应用对应独立的 API 令牌,实现精细化的权限控制和业务隔离。

💡 核心概念:应用是令牌的上级容器,必须先创建应用,才能在应用下创建令牌。每个应用代表一个独立的业务系统或第三方接入方。

应用管理架构

层级 对象 数量限制 说明
1级 企业用户 1个 完成企业认证的企业账号
2级 应用 最多300个 每个企业用户可创建的应用数量上限
3级 令牌 每个应用1个 每个应用只能创建一个 API 令牌

应用场景示例

创建应用

企业用户登录后,进入应用管理页面,按照以下步骤创建新应用:

创建流程

应用创建步骤

1
进入应用管理 → 点击"应用管理"菜单进入应用列表页面
2
点击新建应用 → 填写应用名称、应用描述等基本信息
3
配置回调地址 → 设置允许的白名单域名和IP地址
4
保存应用 → 系统生成唯一的应用标识(App ID)

应用信息字段说明

字段名 必填 说明 示例
应用名称 应用的显示名称,建议体现业务含义 "官网登录系统"
应用描述 应用的详细说明,便于后续管理 "用于公司官网用户登录认证"
回调域名 允许回调的域名白名单,多个用换行分隔 example.com
回调IP 允许回调的IP白名单,支持带端口 127.0.0.1:8080

⚠️ 重要提示:应用名称创建后不可修改,请谨慎命名。建议采用"业务系统+环境"的命名规范,如"官网-生产环境"。

应用列表管理

企业用户可以在应用列表页面查看和管理所有已创建的应用:

应用列表功能

功能 说明
查看详情 查看应用的基本信息、回调配置、令牌状态
编辑配置 修改应用描述、回调域名/IP白名单
创建令牌 为应用创建 API 令牌(每个应用限1个)
禁用/启用 临时禁用应用,禁用后该应用的令牌失效
删除应用 删除应用及其关联的令牌(不可恢复)

应用状态说明

状态 图标 说明
正常 🟢 应用正常运行,令牌有效
已禁用 应用被禁用,令牌失效
无令牌 🟡 应用已创建但未生成令牌

创建令牌

应用创建成功后,需要为应用创建 API 令牌才能进行接口调用。每个应用只能创建一个令牌。

令牌创建流程

令牌创建步骤

1
选择应用 → 在应用列表中找到目标应用
2
点击创建令牌 → 系统生成 API Key 和 API Secret
3
保存密钥立即复制保存 API Secret(仅显示一次)
4
配置接入 → 使用 API Key 和 Secret 进行接口调用

令牌信息说明

字段 说明 使用场景
API Key 客户端标识(Client ID) 授权请求、令牌交换、用户信息获取
API Secret 客户端密钥(Client Secret) 服务器端令牌交换(严禁前端暴露

🔐 安全警告:API Secret 仅在创建时显示一次,请务必立即保存。如遗失需要删除旧令牌并重新创建。

令牌管理

令牌创建后,企业用户可以在应用详情页查看和管理令牌状态:

令牌管理操作

操作 说明 影响
查看 API Key 随时查看 API Key(Client ID) 无影响,可公开使用
重置令牌 删除旧令牌,创建新令牌 旧令牌立即失效,需更新所有接入方配置
删除令牌 删除当前应用的令牌 该应用的所有接口调用将失败

最佳实践

  1. 定期轮换:建议每 90 天重置一次令牌,降低密钥泄露风险
  2. 环境隔离:不同环境(开发/测试/生产)使用不同应用和令牌
  3. 权限最小化:按业务需求配置回调白名单,避免使用通配符
  4. 安全存储:API Secret 存储在服务器环境变量或密钥管理系统中
  5. 监控告警:关注令牌的使用情况和异常调用日志

限制说明

限制项 上限 说明
应用数量 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密钥,强烈建议配置回调地址白名单:

通用安全建议

常见问题

Q: 授权码有效期是多久?

A: 授权码有效期为10分钟,且只能使用一次。

Q: 访问令牌过期了怎么办?

A: 可以使用刷新令牌获取新的访问令牌,无需用户再次授权。

Q: 刷新令牌也过期了怎么办?

A: 需要重新引导用户进行授权流程。

Q: 如何获取用户的手机号?

A: 出于隐私保护,金牛ID不提供用户手机号获取接口。

Q: 为什么提示"回调地址不在允许的域名/IP列表中"?

A: 为了安全起见,金牛ID要求所有回调地址必须在白名单中注册。请前往API密钥管理页面,在"回调地址白名单"部分添加您的回调地址。

Q: 如何配置本地开发环境的回调地址?

A: 在回调地址白名单中添加:

Q: 通配符域名如何使用?

A: 使用 *.example.com 可以匹配所有子域名,如 app.example.comapi.example.com 等。注意:通配符只匹配子域名,不匹配主域名本身。

Q: 配置白名单后多久生效?

A: 配置立即生效,无需等待。

如有其他问题,请联系客服:Jenico.Li | Jenico@vip.qq.com