小红书开放能力调用手册

网页双签名并唤起小红书分享

按官方 Java 工具移植完整 SHA-256 双签名,在服务端缓存分享 token,网页只获取配置并让用户主动分享图片。

官方资料核对 2026-10-15示例代码,未进行真实接口实测
← 返回平台功能

接入准备

  • 已通过分享开放平台企业及应用审核,持有该应用的 appKey/appSecret;本页不适用于小程序或电商的凭证。
  • 安装 Node.js 22,准备自己有权分享且可公开读取的 HTTPS 图片地址。官方图文分享要求服务器图片地址,不接受本地文件路径。
  • 示例仅绑定本机地址用于理解代码;要在手机实测,需将相同服务部署至自己的已登记/获准 HTTPS 应用域名,完成相应配置并加上用户认证、限流与可信来源校验。

准备环境变量

示例值需要替换为自己的凭证或资源 ID。代码在本机或服务端运行;密钥不应放进公开网页。

XHS_SHARE_APP_KEY
分享开放平台审核后的 appKey,与小程序 appid 分开。
XHS_SHARE_APP_SECRET
分享应用 appSecret,只在服务端使用。
XHS_SHARE_IMAGE_URL
替换为自己可公开访问、获授权的真实 HTTPS 图片;这里不是可用素材。
终端
export XHS_SHARE_APP_KEY='your_approved_share_app_key'
export XHS_SHARE_APP_SECRET='replace_with_share_app_secret'
export XHS_SHARE_IMAGE_URL='https://your-approved-domain.example/share-image.jpg'

以上 export 适用于 macOS / Linux 的终端。按调用步骤保存代码文件,再在同一终端执行。

调用步骤

  1. 01

    设置下方三项环境变量,把完整代码保存为 share-server.mjs,运行 node share-server.mjs;本机示例只监听 127.0.0.1:3100。

  2. 02

    打开本机首页可查看按钮结构。业务发生时服务端先用 appSecret 对 appKey、nonce、timeStamp 按官方规则签名,向官方分享令牌端点请求并缓存 token。

  3. 03

    用户点击按钮后获取同源 /share-config:服务端用该分享 access_token 作为第二次签名的 secret,前端只收到 appKey、nonce、timestamp、signature。

  4. 04

    正式部署到自己获准的 HTTPS 域名后在小红书客户端环境检查唤起、图片和用户确认流程;SDK 未报错不代表笔记已经发布。

调用 01

服务端双签名与前端 xhs.share

下方地址是官方 JS SDK 资源入口,不是后台业务 REST 接口。完整代码包含服务端 POST https://edith.xiaohongshu.com/api/sns/v1/ext/access/token、缓存及网页 xhs.share 调用;不传官方已停止自动填充的标题、文案、话题。

官方 SDK 脚本入口;示例包含服务端签名和前端调用。

SDKhttps://fe-static.xhscdn.com/biz-static/goten/xhs-1.0.1.js

SDK 调用参数

参数表可左右滑动,查看完整说明。

字段 / 位置类型 / 必填说明
shareInfo.typeSDK 参数string必填normal 为图文;video 为视频,需要对应媒体字段。本例图文。
shareInfo.imagesSDK 参数string[]必填图文场景必传;必须为服务器图片地址,不是本地文件。
verifyConfig.appKeySDK 参数string必填自己的已获准分享应用 appKey。
verifyConfig.nonce / timestampSDK 参数string必填服务端生成的随机字符串与毫秒时间戳,与第二次签名使用值严格相同。
verifyConfig.signatureSDK 参数string必填按 ASCII 键名拼 appKey、nonce、timeStamp,再直接追加分享 access_token 后 SHA-256,小写十六进制。
failSDK 参数function可选官方仅列调用失败回调;本页不虚构发布成功回调。

调用代码

先在服务端保管密钥并生成签名,再通过前端页面调用 SDK;按上方步骤配置自己的应用和获准域名。

Node.js
// Node.js 22,保存为 share-server.mjs 后执行 node share-server.mjs。
// 本示例仅绑定本机 127.0.0.1:3100。真正手机测试需迁移到自己的获准 HTTPS 应用域名。
import { createServer } from 'node:http';
import { createHash, randomBytes } from 'node:crypto';
const appKey = process.env.XHS_SHARE_APP_KEY;
const appSecret = process.env.XHS_SHARE_APP_SECRET;
const media = process.env.XHS_SHARE_IMAGE_URL;
if (!appKey || !appSecret || !media) throw new Error('缺少分享平台 appKey、appSecret 或图片 URL');
if (new URL(media).protocol !== 'https:') throw new Error('请使用自己有权分享且可公开读取的 HTTPS 图片');
function signature(nonce, timestamp, secret) {
  // 严格对应官方 Java 工具:键名 ASCII 排序后拼原始值,末尾直接追加 secret。
  // timeStamp 的 S 大写;这是 SHA-256,不是 HMAC,也不进行 URL 转义。
  const values = { appKey, nonce, timeStamp: String(timestamp) };
  const base = Object.keys(values).sort().map(k => `${k}=${values[k]}`).join('&') + secret;
  return createHash('sha256').update(base, 'utf8').digest('hex');
}
let cached = null;
let pending = null;
async function accessToken() {
  if (cached && cached.expiresAt > Date.now() + 60000) return cached.token;
  if (pending) return pending;
  pending = (async () => {
    const nonce = randomBytes(16).toString('hex');
    const timestamp = Date.now();
    const response = await fetch('https://edith.xiaohongshu.com/api/sns/v1/ext/access/token', {
      method: 'POST', redirect: 'error', signal: AbortSignal.timeout(20000),
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ app_key: appKey, nonce, timestamp,
        signature: signature(nonce, timestamp, appSecret) }),
    });
    if (!response.ok) throw new Error(`分享凭证 HTTP ${response.status}`);
    const result = await response.json();
    // 官方文档列出 token 字段,未给统一外层报文;兼容直接字段或 data 包装,缺项则失败。
    if (result.success === false || (Object.hasOwn(result, 'code') && result.code !== 0)) {
      throw new Error('分享平台拒绝凭证请求,请核对获准应用与签名');
    }
    const data = result.data ?? result;
    const expiresAt = Number(data.expires_in); // 官方是截止时间戳(毫秒),不是剩余秒数。
    if (typeof data.access_token !== 'string' || !data.access_token ||
        !Number.isFinite(expiresAt) || expiresAt <= Date.now() + 60000) {
      throw new Error('分享凭证字段缺失或已过期,不生成前端签名');
    }
    cached = { token: data.access_token, expiresAt };
    return cached.token;
  })();
  try { return await pending; } finally { pending = null; }
}
const imageJSON = JSON.stringify(media).replaceAll('<', '\\u003c');
const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>用户主动分享到小红书</title>
<button id="share">分享到小红书</button><p id="status" role="status"></p>
<script src="https://fe-static.xhscdn.com/biz-static/goten/xhs-1.0.1.js"></script>
<script>
const button = document.querySelector('#share');
const status = document.querySelector('#status');
button.onclick = async () => {
  button.disabled = true;
  try {
    if (!window.xhs || typeof window.xhs.share !== 'function') throw new Error('官方 SDK 未加载');
    const res = await fetch('/share-config', { cache: 'no-store' });
    if (!res.ok) throw new Error('获取分享配置失败');
    const verifyConfig = await res.json();
    status.textContent = '已请求唤起;请在小红书 App 内检查内容并自行完成发布';
    window.xhs.share({ shareInfo: { type: 'normal', images: [${imageJSON}] }, verifyConfig,
      fail: () => { status.textContent = '分享唤起失败,请检查客户端、应用审核和配置'; } });
  } catch (error) { status.textContent = error.message; }
  finally { button.disabled = false; }
};
</script></html>`;
const server = createServer(async (req, res) => {
  // 生产部署需加入自己的用户会话校验、请求限流与可信来源校验,不能做公开任意签名代理。
  res.setHeader('Cache-Control', 'no-store');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  if (req.method !== 'GET') { res.writeHead(405); res.end(); return; }
  if (req.url === '/') { res.setHeader('Content-Type', 'text/html;charset=utf-8'); res.end(html); return; }
  if (req.url !== '/share-config') { res.writeHead(404); res.end(); return; }
  try {
    const token = await accessToken();
    const nonce = randomBytes(16).toString('hex');
    const timestamp = String(Date.now());
    // 第二次签名的 secret 用 access_token,绝不能再次使用 appSecret。
    const config = { appKey, nonce, timestamp, signature: signature(nonce, timestamp, token) };
    res.setHeader('Content-Type', 'application/json;charset=utf-8');
    res.end(JSON.stringify(config)); // 不向前端输出 appSecret 或 access_token。
  } catch {
    res.writeHead(502, { 'Content-Type': 'application/json;charset=utf-8' });
    res.end(JSON.stringify({ error: '分享配置生成失败,请在服务端检查应用和签名' }));
  }
});
server.listen(3100, '127.0.0.1', () => console.log('本机示例:http://127.0.0.1:3100/'));

回调示例

SDK 不是 JSON REST 响应;官方只列 fail(e) 回调,没有发布成功的返回结构。下面为回调形式示意,不代表已发布。 以下为结构示意,非本站实测结果。

JavaScript
fail(e) { /* SDK 调用失败时执行;核对应用、签名、媒体和客户端 */ }

常见错误与排查

本地:SDK 未加载window.xhs.share 不存在
核对页面确实加载官方脚本,网络或 CSP 未拦截;不能用非官方脚本替换。
本地:分享配置生成失败本例后端拒绝输出配置
核对分享应用审核和密钥、签名键名 timeStamp、毫秒时间戳、原始值不做 URL 转义;官方令牌文档未列完整错误码表,不编造数字。
fail(e)官方 SDK 失败回调
服务端第二次签名必须使用分享 access_token;前端 nonce/timestamp 必须与加签相同,媒体需为真实服务器地址。

调用注意

  • 官方 Java 实现是三个参数键名排序后 key=value 用 & 连接,最后追加 secret,不把 secret 当作 appSecret=xxx 参数加入,也不是 HMAC。
  • 第一次签名末尾使用 appSecret,POST 字段为 app_key、nonce、timestamp、signature;第二次末尾使用分享 access_token。
  • 分享 token 的 expires_in 是有效截止毫秒时间戳,最长24小时;重复获取会覆盖历史 token,本例用缓存与单进程并发合并减少冲突,生产需集中缓存锁。
  • 官方 token 文档列字段但未列统一外层报文,示例兼容直接字段和 data 包装,缺失有效字段或过期均失败;此兼容策略未经真实接口验证。
  • 服务器仅供本机理解;正式对外部署必须按业务做访问控制,避免公共签名服务。前端永不返回 appSecret 或 access_token。

注意事项

  • 网页分享 SDK 不支持后台静默发布,不提供任意笔记或评论读取能力;用户仍须在 App 内确认。
  • 官方顶部停止自动填充文字的提醒优先于旧参数列中的 title/content 示例,本页仅传媒体。
  • 未执行真实令牌请求或在手机客户端实测,代码语法和离线 mock 不证明应用已获准。

核验来源

本站按官方资料整理本场景的必要调用步骤与代码,覆盖范围以本页列出的接口为准。官方资料用于追溯规范及查看变更。

  1. 01

小红书开放能力的其他调用手册

← 返回小红书开放能力的全部功能