跳到主要内容

OAuth 与开放 API

本文档介绍第三方应用如何通过 OAuth 获取授权,并使用 /open 开放 API 访问用户侧资源。

信息

该文档部分内容使用生成式人工智能编写。

基础信息

项目地址
API Base URLhttps://api.console.nrtun.com
OAuth 浏览器授权端点https://console.nrtun.com/oauth/authorize
OAuth Token 端点https://api.console.nrtun.com/oauth2/token
UserInfo 端点https://api.console.nrtun.com/oauth2/userinfo
JWKShttps://api.console.nrtun.com/oauth2/jwks.json
OIDC Discoveryhttps://api.console.nrtun.com/oauth2/.well-known/openid-configuration
开放 API 前缀https://api.console.nrtun.com/open

OAuth 授权页和 Device Code 确认页是用户交互页面,使用 console.nrtun.com 控制台域名。Token、UserInfo、JWKS、Device Code 申请和开放 API 均为服务端接口,继续使用 api.console.nrtun.com API 域名。

支持的授权方式

当前支持以下 OAuth 授权方式:

grant_type用途是否推荐
authorization_codeWeb 服务端应用、CLI、桌面应用、移动端应用推荐
refresh_token刷新用户授权 access token推荐
urn:ietf:params:oauth:grant-type:device_code无浏览器或输入受限设备按需使用
client_credentials机器到机器调用仅限 client 类 scope,不可访问用户侧 /open/tunnels
注意

client_credentials 不会继承 OAuth 应用所有者的用户权限,也不能用于 tunnel:read / tunnel:write 这类用户侧 scope。访问用户自己的隧道必须使用用户参与授权的流程,例如 authorization_code 或 device code。

应该选择哪种授权方式

场景推荐方式说明
Web 服务端应用接入 NatureTunnel 登录或开放 APIAuthorization Code + client_secret服务端可以安全保存密钥,流程最标准
SPA、移动端、桌面端、CLIAuthorization Code + PKCE无法安全保存 client_secret 时使用
路由器、电视、服务器终端等输入受限设备Device Code用户在另一台设备浏览器中确认授权
后台服务调用应用自身资源Client Credentials只适合应用自身身份,不能访问用户隧道
access token 过期后续期Refresh Token前提是前一次授权返回了 refresh_token

Token 端点认证方式

/oauth2/token 支持两种提交客户端密钥的方式:

client_secret_basic

client_id:client_secret 做 Base64 编码后放入 Authorization: Basic

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=nrtun_rt_xxx'

client_secret_post

client_idclient_secret 放在请求体中:

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET' \
-d 'refresh_token=nrtun_rt_xxx'
提示

服务端应用建议优先使用 client_secret_basic。public client 使用 PKCE 时不提交 client_secret

Scope 列表

Scope说明可访问能力
profile读取用户基础资料/oauth2/userinfoGET /open/me
permissions读取 token 中的权限列表/oauth2/userinfo 返回 permissions
tunnel:read读取授权用户自己的隧道GET /open/tunnelsGET /open/tunnels/:tunnelId
tunnel:write编辑授权用户自己的隧道包含 tunnel:read 能力,并可调用 PATCH /open/tunnels/:tunnelId
certificate:read读取授权用户自己的证书和私钥GET /open/certificatesGET /open/certificates/:certificateId

创建 OAuth 应用

登录控制台后,在 OAuth 应用管理页面创建应用。创建后会得到:

  • client_id
  • client_secret
  • 已配置的 redirect_uri

请妥善保管 client_secret。如果你的应用是浏览器前端、移动端、CLI 等无法安全保存密钥的 public client,请使用 PKCE。

Authorization Code 流程

1. 跳转到授权页

将用户重定向到授权端点:

GET https://console.nrtun.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=code&scope=profile%20tunnel%3Aread&state=RANDOM_STATE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256

参数说明:

参数必填说明
client_idOAuth 应用 ID
redirect_uri必须与应用中配置的回调地址完全一致
response_type建议固定使用 code
scope使用空格分隔,例如 profile tunnel:read
state建议用于防 CSRF,回调时原样返回
code_challengepublic client 必填PKCE challenge
code_challenge_methodpublic client 必填当前只支持 S256
提示

如果未传 scope,默认会包含 profile

2. 用户确认授权

用户登录并同意授权后,服务端会将用户带回你的 redirect_uri,并附带授权码:

https://example.com/callback?code=AUTH_CODE&state=RANDOM_STATE

如果用户拒绝授权,会返回:

https://example.com/callback?error=access_denied&state=RANDOM_STATE

3. 使用授权码换取 token

服务端应用可以使用 client_secret

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
-d 'grant_type=authorization_code' \
-d 'code=AUTH_CODE' \
-d 'redirect_uri=https://example.com/callback'

Public client 使用 PKCE,不需要提交 client_secret

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'code=AUTH_CODE' \
-d 'redirect_uri=https://example.com/callback' \
-d 'code_verifier=CODE_VERIFIER'

成功响应示例:

{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "nrtun_rt_xxx",
"scope": "profile tunnel:read"
}

PKCE 生成方式

code_verifier 是一个高熵随机字符串,code_challenge 为它的 SHA-256 Base64URL 编码。

Node.js 示例:

import crypto from "node:crypto";

const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");

刷新 access token

access_token 过期后,可以使用 refresh_token 换取新 token:

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=nrtun_rt_xxx'

如果是 public client,可以不传 client_secret,但必须传 client_id

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'refresh_token=nrtun_rt_xxx'

响应示例:

{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "nrtun_rt_new_xxx",
"scope": "profile tunnel:read"
}
备注

刷新时旧的 refresh_token 会被吊销,客户端应保存新返回的 refresh_token

Device Code 流程

Device Code 适合没有浏览器或输入不方便的设备,例如路由器、电视、终端程序、服务器命令行工具。

1. 设备申请 device code

curl -X POST https://api.console.nrtun.com/oauth2/device/code \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'scope=profile tunnel:read'

响应示例:

{
"device_code": "nrtun_dc_xxx",
"user_code": "A1B2C3D4",
"verification_uri": "https://console.nrtun.com/oauth/device",
"verification_uri_complete": "https://console.nrtun.com/oauth/device?user_code=A1B2C3D4",
"expires_in": 900,
"interval": 5
}

字段说明:

字段说明
device_code设备轮询 token 端点时使用,不能展示给最终用户
user_code展示给用户输入的短码
verification_uri用户打开后输入 user_code 完成授权
verification_uri_complete已带上 user_code 的授权地址,可直接展示二维码
expires_in设备码有效期,单位秒
interval轮询间隔,单位秒

2. 提示用户完成授权

设备应提示用户打开 verification_uri_complete,或打开 verification_uri 后输入 user_code

示例提示:

请在浏览器打开:
https://console.nrtun.com/oauth/device?user_code=A1B2C3D4

或访问 https://console.nrtun.com/oauth/device 后输入验证码:A1B2C3D4

用户登录 NatureTunnel 控制台账号并确认授权后,设备即可换取 token。

3. 设备轮询 token 端点

设备按照响应中的 interval 轮询 /oauth2/token

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'device_code=nrtun_dc_xxx'

用户尚未完成授权时,可能返回 authorization_pending。设备应等待 interval 秒后重试,不要高频轮询。

授权完成后的响应示例:

{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "nrtun_rt_xxx",
"scope": "profile tunnel:read"
}
注意

Device Code 获取到的是用户授权 token,因此可以访问用户侧 /open API。设备应安全保存 refresh_token,不要将其打印到日志或上传到第三方服务。

CLI/TUI 在完成 OAuth Device Code 登录后,可以使用 access_token 调用 GET /open/tunnelsGET /open/tunnels/:tunnelId 获取 TCP/UDP/HTTP 隧道启动配置。若接口返回 401,应使用 refresh_token 调用 /oauth2/token 刷新 access_token,保存响应中的新 refresh_token,然后重试一次原请求。

Client Credentials 流程

client_credentials 适用于应用自身身份,不适用于访问用户资源。

典型用途:

  • 应用后台任务调用平台提供的应用级接口
  • 服务之间通信
  • 不涉及具体 NatureTunnel 用户资源的自动化任务

不适合:

  • 读取用户资料
  • 读取或编辑用户隧道
  • 代替某个用户进行操作

请求示例:

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
-d 'grant_type=client_credentials' \
-d 'scope=client:example'

也可以使用 client_secret_post

curl -X POST https://api.console.nrtun.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET' \
-d 'scope=client:example'

响应示例:

{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "client:example"
}
注意

如果请求 profiletunnel:readtunnel:write 等用户侧 scope,会被拒绝。client_credentials 不会继承应用所有者的用户权限。

吊销 token

如果 access token 或 refresh token 泄漏,或者用户主动解除授权,应用应调用 /oauth2/revoke 吊销 token。

curl -X POST https://api.console.nrtun.com/oauth2/revoke \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'token=TOKEN_TO_REVOKE'

可以吊销:

  • OAuth access token
  • OAuth refresh token

响应示例:

{
"success": true,
"data": null,
"message": "令牌已吊销"
}
备注

当前吊销接口不要求客户端认证。请不要把 token 暴露给不可信环境,否则任何拿到 token 的人都可能吊销它。

获取用户信息

curl https://api.console.nrtun.com/oauth2/userinfo \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"sub": "USER_ID",
"username": "nrtun-user",
"email": "user@example.com",
"avatar": "avatar/file/key",
"avatar_url": "https://...",
"scope": "profile tunnel:read",
"permissions": ["open.tunnel.read"]
}

开放 API 调用方式

所有 /open 接口都使用 OAuth access token:

Authorization: Bearer YOUR_ACCESS_TOKEN
注意

不要使用控制台登录后的主站 JWT 调用 /open/open 只接受 OAuth access token。

GET /open/me

读取授权用户的基础信息。

所需 scope:profile

curl https://api.console.nrtun.com/open/me \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"success": true,
"data": {
"id": "USER_ID",
"username": "nrtun-user",
"email": "user@example.com",
"avatar": "avatar/file/key",
"scopes": ["profile", "tunnel:read"],
"permissions": ["open.tunnel.read"],
"isIdentityVerified": true
},
"message": "获取用户信息成功"
}

GET /open/tunnels

获取当前授权用户自己的隧道数组,数组元素结构与 GET /open/tunnels/:tunnelId 返回的 tunnel 对象一致。

所需 scope:tunnel:read

所需 permission:open.tunnel.read

curl https://api.console.nrtun.com/open/tunnels \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"success": true,
"data": [
{
"id": "TUNNEL_ID",
"status": "active",
"tunnelName": "my-tunnel",
"userId": "USER_ID",
"bindNodeId": "NODE_ID",
"nodeName": "Node 1",
"nodeIP": "203.0.113.10",
"nodePort": 2333,
"nodeDataPort": 2334,
"userSecret": "USER_SECRET",
"remotePort": 12345,
"httpHost": "demo.example.com",
"protocol": "tcp",
"localHost": "127.0.0.1",
"localPort": 8080,
"options": {
"isAllowOpenInClient": true,
"isBanned": false,
"proxyProtocol": {
"enabled": false,
"version": "v2"
},
"bindCertId": "CERT_ID"
},
"createAt": 1760000000000,
"lastActivity": 1760000000000
}
],
"message": "获取隧道列表成功"
}

GET /open/tunnels/:tunnelId

获取指定隧道详情。路径参数 tunnelId 为隧道 ID。

所需 scope:tunnel:read

所需 permission:open.tunnel.read

curl https://api.console.nrtun.com/open/tunnels/TUNNEL_ID \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"success": true,
"data": {
"id": "TUNNEL_ID",
"status": "active",
"tunnelName": "my-tunnel",
"userId": "USER_ID",
"bindNodeId": "NODE_ID",
"nodeName": "Node 1",
"nodeIP": "203.0.113.10",
"nodePort": 2333,
"nodeDataPort": 2334,
"userSecret": "USER_SECRET",
"remotePort": 12345,
"httpHost": "demo.example.com",
"protocol": "tcp",
"localHost": "127.0.0.1",
"localPort": 8080,
"options": {
"isAllowOpenInClient": true,
"isBanned": false,
"proxyProtocol": {
"enabled": false,
"version": "v2"
},
"bindCertId": "CERT_ID"
},
"createAt": 1760000000000,
"lastActivity": 1760000000000
},
"message": "获取隧道成功"
}

错误响应:

HTTP 状态码说明
401access token 缺失、无效或过期
403缺少 tunnel:read scope 或 open.tunnel.read permission
404隧道不存在或不属于当前授权用户

OpenTunnel

GET /open/tunnelsdata 数组元素和 GET /open/tunnels/:tunnelIddata 均使用 OpenTunnel 结构。

字段类型说明
idstring隧道 ID
statusstring隧道状态,例如 active / disconnect
tunnelNamestring隧道名称
userIdstring隧道所属用户 ID
bindNodeIdstring绑定节点 ID
nodeNamestring绑定节点名称
nodeIPstring客户端连接节点控制服务的地址,来源为 node.options.ConnectIP
nodePortnumber客户端连接节点控制服务的端口,来源为 node.options.ConnectPort
nodeDataPortnumber客户端连接节点数据服务的端口,来源为 node.options.DataPort
userSecretstring当前用户 secret,当前后端仍会返回,但迁移 OAuth-only 客户端时不建议继续依赖
remotePortnumberTCP/UDP 隧道远程端口;HTTP/HTTPS 隧道通常为 0
httpHoststringHTTP/HTTPS 隧道域名;TCP/UDP 隧道通常为空字符串
protocoltcp / udp / http / https隧道协议
localHoststring客户端本地目标地址
localPortnumber客户端本地目标端口
optionsobject隧道选项
options.isAllowOpenInClientboolean是否允许客户端打开
options.isBannedboolean是否被封禁
options.proxyProtocolobjectProxy Protocol 配置,仅 tcp / udp 使用
options.proxyProtocol.enabledboolean是否启用 Proxy Protocol
options.proxyProtocol.versionv1 / v2Proxy Protocol 版本
options.bindCertIdstringHTTPS 隧道绑定的证书 ID,如果未绑定可能不存在
createAtnumber创建时间戳
lastActivitynumber最近活动时间戳
注意

userSecret 当前由后端返回是为了兼容历史客户端字段,但 OAuth-only 客户端不应依赖该字段作为长期方案。后续如果后端移除该字段,客户端应不受影响。

OpenAPI schema

paths:
/open/tunnels:
get:
summary: 获取隧道列表
security:
- bearerAuth: []
responses:
"200":
description: 获取隧道列表成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
type: array
items:
$ref: "#/components/schemas/OpenTunnel"
message:
type: string
example: 获取隧道列表成功
"401":
description: access token 缺失、无效或过期
"403":
description: 缺少 tunnel:read scope 或 open.tunnel.read permission
/open/tunnels/{tunnelId}:
get:
summary: 获取隧道详情
security:
- bearerAuth: []
parameters:
- name: tunnelId
in: path
required: true
schema:
type: string
responses:
"200":
description: 获取隧道成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
$ref: "#/components/schemas/OpenTunnel"
message:
type: string
example: 获取隧道成功
"401":
description: access token 缺失、无效或过期
"403":
description: 缺少 tunnel:read scope 或 open.tunnel.read permission
"404":
description: 隧道不存在或不属于当前授权用户
/open/certificates:
get:
summary: 获取证书列表
security:
- bearerAuth: []
responses:
"200":
description: 获取证书列表成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
type: array
items:
$ref: "#/components/schemas/OpenCertificateMetadata"
message:
type: string
example: 获取证书列表成功
"401":
description: access token 缺失、无效或过期
"403":
description: 缺少 certificate:read scope、open.certificate.read permission 或未完成实名认证
/open/certificates/{certificateId}:
get:
summary: 下载证书和私钥内容
security:
- bearerAuth: []
parameters:
- name: certificateId
in: path
required: true
schema:
type: string
responses:
"200":
description: 获取证书成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
$ref: "#/components/schemas/OpenCertificateDetail"
message:
type: string
example: 获取证书成功
"401":
description: access token 缺失、无效或过期
"403":
description: 缺少 certificate:read scope、open.certificate.read permission 或未完成实名认证
"404":
description: 证书不存在或不属于当前授权用户
components:
schemas:
OpenTunnel:
type: object
required:
- id
- status
- tunnelName
- userId
- bindNodeId
- nodeName
- nodeIP
- nodePort
- nodeDataPort
- userSecret
- remotePort
- httpHost
- protocol
- localHost
- localPort
- options
- createAt
- lastActivity
properties:
id:
type: string
status:
type: string
tunnelName:
type: string
userId:
type: string
bindNodeId:
type: string
nodeName:
type: string
nodeIP:
type: string
nodePort:
type: number
nodeDataPort:
type: number
userSecret:
type: string
remotePort:
type: number
httpHost:
type: string
protocol:
type: string
enum: [tcp, udp, http, https]
localHost:
type: string
localPort:
type: number
options:
type: object
properties:
isAllowOpenInClient:
type: boolean
isBanned:
type: boolean
proxyProtocol:
type: object
properties:
enabled:
type: boolean
version:
type: string
enum: [v1, v2]
bindCertId:
type: string
createAt:
type: number
lastActivity:
type: number
OpenCertificateMetadata:
type: object
required:
- id
- domain
- status
- source
- issuedAt
- expiresAt
- createdAt
- updatedAt
properties:
id:
type: string
domain:
type: string
status:
type: string
enum: [pending, valid, expired, revoked]
source:
type: string
enum: [system, uploaded]
issuedAt:
type: [string, "null"]
format: date-time
expiresAt:
type: [string, "null"]
format: date-time
createdAt:
type: [string, "null"]
format: date-time
updatedAt:
type: [string, "null"]
format: date-time
OpenCertificateDetail:
allOf:
- $ref: "#/components/schemas/OpenCertificateMetadata"
- type: object
required:
- contentEncoding
- certificateBase64
- privateKeyBase64
properties:
contentEncoding:
type: string
const: base64
certificateBase64:
type: string
contentEncoding: base64
privateKeyBase64:
type: string
contentEncoding: base64

GET /open/certificates

获取当前授权用户自己的证书列表。列表只包含证书元数据,不返回证书或私钥内容。

所需 scope:certificate:read

所需 permission:open.certificate.read

其他要求:必须使用用户授权 token,并且授权用户已完成实名认证。

curl https://api.console.nrtun.com/open/certificates \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"success": true,
"data": [
{
"id": "CERTIFICATE_ID",
"domain": "example.com",
"status": "valid",
"source": "system",
"issuedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2027-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:01.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
],
"message": "获取证书列表成功"
}

证书元数据字段:

字段类型说明
idstring证书 ID,可传给 GET /open/certificates/:certificateId
domainstring证书域名
statuspending / valid / expired / revoked证书状态
sourcesystem / uploaded证书来源,分别表示系统申请或用户上传
issuedAtstring / null签发时间,ISO 8601 格式;尚未签发时为 null
expiresAtstring / null过期时间,ISO 8601 格式;未知时为 null
createdAtstring / null创建时间,ISO 8601 格式
updatedAtstring / null更新时间,ISO 8601 格式

GET /open/certificates/:certificateId

下载当前授权用户自己的证书及私钥内容。路径参数 certificateId 为证书 ID,可以来自 GET /open/certificates,也可以使用 HTTPS 隧道详情中的 options.bindCertId

所需 scope:certificate:read

所需 permission:open.certificate.read

其他要求:必须使用用户授权 token,并且授权用户已完成实名认证。

curl https://api.console.nrtun.com/open/certificates/CERTIFICATE_ID \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

响应示例:

{
"success": true,
"data": {
"id": "CERTIFICATE_ID",
"domain": "example.com",
"status": "valid",
"source": "system",
"issuedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2027-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:01.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z",
"contentEncoding": "base64",
"certificateBase64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tLi4u",
"privateKeyBase64": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tLi4u"
},
"message": "获取证书成功"
}

内容字段:

字段类型说明
contentEncodingbase64certificateBase64privateKeyBase64 的编码方式,当前固定为 Base64
certificateBase64string证书 PEM 内容经过 UTF-8 Base64 编码后的字符串
privateKeyBase64string私钥 PEM 内容经过 UTF-8 Base64 编码后的字符串

接口返回 JSON,不会直接返回文件流。Node.js 解码并保存为 PEM 文件的示例:

import { writeFile } from "node:fs/promises";

const response = await fetch(
"https://api.console.nrtun.com/open/certificates/CERTIFICATE_ID",
{
headers: {
Authorization: "Bearer YOUR_ACCESS_TOKEN",
},
},
);

if (!response.ok) {
throw new Error(`下载证书失败: HTTP ${response.status}`);
}

const result = await response.json();
const certificate = result.data;

await writeFile(
"certificate.pem",
Buffer.from(certificate.certificateBase64, "base64"),
{ mode: 0o600 },
);
await writeFile(
"private-key.pem",
Buffer.from(certificate.privateKeyBase64, "base64"),
{ mode: 0o600 },
);
注意

该接口会返回未加密的私钥内容。不要记录完整响应、Base64 字段或解码后的私钥,不要将其暴露给浏览器页面、日志、错误上报或不可信的第三方服务。服务端会为详情响应设置 Cache-Control: no-storePragma: no-cache

如果证书不存在或不属于当前授权用户,接口统一返回 404。尚未签发完成的证书可能返回空的 certificateBase64privateKeyBase64,调用方应先检查 status 和内容字段。

HTTPS 隧道证书获取流程

  1. 调用 GET /open/tunnels/:tunnelId 获取 HTTPS 隧道详情。
  2. 读取 options.bindCertId;未返回该字段表示隧道未绑定证书。
  3. 使用包含 certificate:read scope 的用户授权 token 调用 GET /open/certificates/:certificateId
  4. contentEncoding 解码并安全保存证书和私钥,然后启动 HTTPS 隧道客户端。

TCP / UDP / HTTP 隧道可以直接使用现有隧道读取接口返回的主要运行配置。当前后端没有独立的 runtime config 接口,客户端应使用 GET /open/tunnelsGET /open/tunnels/:tunnelId 返回的字段。

PATCH /open/tunnels/:tunnelId

编辑授权用户自己的隧道。

所需 scope:tunnel:write

curl -X PATCH https://api.console.nrtun.com/open/tunnels/TUNNEL_ID \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"tunnelName": "new-name",
"localHost": "127.0.0.1",
"localPort": 8080,
"options": {
"isAllowOpenInClient": true,
"proxyProtocol": {
"enabled": false,
"version": "v2"
}
}
}'

可编辑字段:

字段类型说明
tunnelNamestring隧道名称
localHoststring本地服务地址
localPortnumber本地服务端口,范围 1-65535
options.isAllowOpenInClientboolean是否允许在客户端打开
options.proxyProtocol.enabledboolean是否启用 Proxy Protocol
options.proxyProtocol.versionv1 / v2Proxy Protocol 版本

不可通过开放 API 编辑:

  • 隧道所属用户
  • 绑定节点
  • 远程端口
  • HTTP 域名
  • 协议类型
  • 证书绑定
  • 封禁状态
备注

Proxy Protocol 只适用于 tcp / udp 隧道。HTTP/HTTPS 隧道不能启用 Proxy Protocol;UDP 隧道仅支持 v2

常见错误

HTTP 状态码常见原因处理方式
401access token 无效、过期或未传 Authorization重新获取或刷新 token
403缺少 scope、缺少 permission、用户被封禁或未完成实名认证检查授权范围和用户状态
404隧道或证书不存在,或者不属于当前授权用户检查资源 ID 和授权用户
400参数不合法,例如 localPort 超出范围按接口文档修正请求体

安全建议

  • 服务端应用应使用 client_secret,不要将 client_secret 暴露给浏览器前端。
  • 浏览器、移动端、CLI 等 public client 应使用 PKCE。
  • 每次授权请求都应带上随机 state 并在回调时校验。
  • 只申请必要 scope,不要让应用默认请求过大的权限范围。
  • access token 泄漏后应立即调用 /oauth2/revoke 吊销。
  • 通过 certificate:read 获取的私钥必须加密存储或限制为仅当前服务账号可读,并避免写入日志和错误上报。