CITV 开放文档
EventHub

CITV 合规业务接口 v2

CITV 合规业务接口 v2 定义与接入说明(双向 RSA 签名)

CITV 合规业务接口 v2

本文档定义了 SaaS 平台向 CITV 开放的合规业务接口 v2 版本,用于封禁特定内容。 v2 版本采用 双向 RSA 签名机制 确保接口安全。

一、接口概要

1. 接口开发与开放

  • 本文档所列明的接口,均需 SaaS 平台客户自行开发,并向 CITV 开放
  • 为了接口安全,可将 CITV IP 地址列表加入接口访问白名单

2. 接口地址

建议在请求路径中使用 v2 以区分 api 版本

接口路径:/ban/v2

3. 接口一览

名称接口说明
封禁ban触发限时整改、即刻封禁操作

二、前置准备(密钥交换)

接口对接前,双方需先完成 RSA 密钥对的生成与交换:

  1. 调用方(CITV):自行生成 RSA 密钥对,将公钥提供给 SaaS 平台
  2. 被调用方(SaaS 平台):自行生成 RSA 密钥对,将公钥提供给 CITV
  3. 双方均使用对方公钥验证签名,使用自己私钥进行签名
  4. 密钥长度要求:4096 位

Go 语言参考示例代码库:citv-ban-demo


双方对接清单

🔴 调用方(CITV)需要提供:

项目说明
RSA 公钥4096 位,用于 SaaS 平台验证 CITV 请求签名

🟢 被调用方(SaaS 平台)需要提供:

项目说明
RSA 公钥4096 位,用于 CITV 验证 SaaS 平台响应签名
appid应用唯一标识,分配给 CITV 用于接口调用
接口地址/ban/v2 封禁接口的完整访问 URL

三、整体交互流程

完整的合规业务接口交互流程如下:

┌────────────┐                                      ┌──────────────────┐
│            │  1. 交换 RSA 公钥                     │                  │
│   CITV     │ ───────────────────────────────────► │   SaaS 平台      │
│  (调用方)  │                                      │  (被调用方)      │
│            │ ◄─────────────────────────────────── │                  │
│            │  2. 提供 appid + 接口地址             │                  │
└──────┬─────┘                                      └────────┬─────────┘
       │                                                     │
       │  3. 构造请求(appid, nonce, timestamp, body)       │
       │                                                     │
       │  4. 调用方私钥签名(SHA256WithRSA)                  │
       │                                                     │
       │  5. POST /ban/v2(带 sign 头)                      │
       └────────────────────────────────────────────────────►┐

                                                      ┌──────▼──────┐
                                                      │  SaaS 平台  │
                                                      └──────┬──────┘

                       ┌──────────────────────────────────────┘
                       │  6. 使用 CITV 公钥验证签名
                       │  7. 验证时间戳有效期(5分钟)
                       │  8. 验证 nonce 防重放
                       │  9. 执行封禁/解封业务逻辑
                       │  10. 生成 ticketId
                       │  11. SaaS 私钥对响应签名


┌────────────┐                                      ┌──────────────────┐
│            ◄────────────────────────────────────── ┘                  │
│   CITV     │  12. 返回响应(带 ticketId, ccid, nonce, timestamp, sign)
│  (调用方)  │                                      │  (被调用方)      │
│            │                                      │                  │
│            │  13. CITV 验证响应签名                │                  │
└────────────┘                                      └──────────────────┘

交互步骤详解

阶段动作负责方说明
对接准备交换 RSA 公钥双方各自生成 4096 位 RSA 密钥对,互相交换公钥
对接准备SaaS 分配 appidSaaSSaaS 为 CITV 分配唯一应用标识 appid
对接准备SaaS 提供接口地址SaaS提供 /ban/v2 接口的完整访问 URL
请求阶段构造请求参数CITV构造 ccid, casn, action, code 等业务参数
请求阶段生成请求签名CITV对 appid, casn, ccid, nonce, timestamp 按字母序签名
请求阶段发送请求CITVPOST 请求,sign/timestamp/nonce/appid 放 Header
处理阶段验证请求签名SaaS使用 CITV 公钥验证签名有效性
处理阶段安全校验SaaS时间戳校验(5分钟内)+ nonce 防重放校验
处理阶段执行业务逻辑SaaS根据 action 执行封禁/整改/解封操作
响应阶段生成响应签名SaaS对 ccid, nonce, ticketId, timestamp 按字母序签名
响应阶段返回响应SaaS返回带 sign 的响应体
响应阶段验证响应签名CITV使用 SaaS 公钥验证响应签名

四、双向签名规则

角色说明

  • 调用方 = CITV(主动发起请求)
  • 被调用方 = SaaS 平台(接收请求并返回响应)

1. 调用方(CITV)请求规则

必填请求头

调用方请求必须包含以下 HTTP 头信息:

参数名称必填说明
sign调用方 RSA 签名,Base64 编码
timestamp请求时间戳(Unix 时间戳格式,13 位)
nonceString 防重放随机串,推荐使用 UUID
appid应用唯一标识,由被调用方分配

调用方签名生成方式

签名范围appidcasnccidnoncetimestamp

签名流程

  1. 按字母序拼接签名原文:appid={appid}&casn={casn}&ccid={ccid}&nonce={nonce}&timestamp={timestamp}
  2. 使用 SHA256withRSA 算法,用调用方 RSA 私钥对签名原文进行签名
  3. 将签名结果进行 Base64 编码,即为 sign

待签名字符串格式示例:

appid=your_app_id&casn=ca1234567890&ccid=cc123456&nonce=abc123xyz&timestamp=1630000000000

Go 签名示例

package main

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
)

func generateSignature(
	appid, casn, ccid, nonce string,
	timestamp int64,
	privateKeyPem string,
) (string, error) {
	signString := fmt.Sprintf("appid=%s&casn=%s&ccid=%s&nonce=%s&timestamp=%d",
		appid, casn, ccid, nonce, timestamp)

	block, _ := pem.Decode([]byte(privateKeyPem))
	if block == nil {
		return "", fmt.Errorf("failed to parse private key PEM")
	}

	privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return "", err
	}

	rsaKey, ok := privateKey.(*rsa.PrivateKey)
	if !ok {
		return "", fmt.Errorf("not an RSA private key")
	}

	hashed := sha256.Sum256([]byte(signString))
	signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hashed[:])
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(signature), nil
}

2. 被调用方(SaaS 平台)处理规则

请求签名验证

被调用方收到请求后:

  1. 使用相同规则拼接待签名字符串
  2. 使用调用方(CITV)RSA 公钥验证签名有效性
  3. 检查时间戳是否在有效期内(建议 5 分钟)
  4. 验证 nonce 唯一性防止重放攻击

响应签名规则

被调用方在响应时,需对 ccidnonceticketIdtimestamp 字段按字母序进行签名返回。

响应签名流程

  1. 按字母序拼接签名原文:ccid={ccid}&nonce={nonce}&ticketId={ticketId}&timestamp={timestamp}
  2. 使用被调用方 RSA 私钥进行 SHA256WithRSA 签名
  3. Base64 编码后放入响应的 sign 字段

待签名字符串格式示例:

ccid=cc123456&nonce=abc123xyz&ticketId=NzajVXCK6vxsiVyh7y&timestamp=1630000000000

TypeScript 响应签名示例

import * as crypto from "crypto";

function generateResponseSignature(
  ccid: string,
  nonce: string,
  ticketId: string,
  timestamp: number,
  privateKeyPem: string
): string {
  const signString = `ccid=${ccid}&nonce=${nonce}&ticketId=${ticketId}&timestamp=${timestamp}`;

  const sign = crypto.createSign("SHA256");
  sign.update(signString, "utf8");
  return sign.sign(privateKeyPem, "base64");
}

Java 响应签名示例

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;

public class RSASigner {
    public static String generateResponseSignature(
            String ccid,
            String nonce,
            String ticketId,
            long timestamp,
            String privateKeyPem) throws Exception {

        String signString = String.format("ccid=%s&nonce=%s&ticketId=%s&timestamp=%d",
                ccid, nonce, ticketId, timestamp);

        String privateKeyContent = privateKeyPem
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s", "");

        byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

        Signature sign = Signature.getInstance("SHA256WithRSA");
        sign.initSign(privateKey);
        sign.update(signString.getBytes(StandardCharsets.UTF_8));

        return Base64.getEncoder().encodeToString(sign.sign());
    }
}

Go 响应签名示例

func generateResponseSignature(
	ccid, nonce, ticketId string,
	timestamp int64,
	privateKeyPem string,
) (string, error) {
	signString := fmt.Sprintf("ccid=%s&nonce=%s&ticketId=%s&timestamp=%d",
		ccid, nonce, ticketId, timestamp)

	block, _ := pem.Decode([]byte(privateKeyPem))
	if block == nil {
		return "", fmt.Errorf("failed to parse private key PEM")
	}

	privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return "", err
	}

	rsaKey, ok := privateKey.(*rsa.PrivateKey)
	if !ok {
		return "", fmt.Errorf("not an RSA private key")
	}

	hashed := sha256.Sum256([]byte(signString))
	signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hashed[:])
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(signature), nil
}

Python 响应签名示例

import base64
import hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

def generate_response_signature(
    ccid: str,
    nonce: str,
    ticket_id: str,
    timestamp: int,
    private_key_pem: str
) -> str:
    sign_string = f"ccid={ccid}&nonce={nonce}&ticketId={ticket_id}&timestamp={timestamp}"

    private_key = serialization.load_pem_private_key(
        private_key_pem.encode(),
        password=None
    )

    signature = private_key.sign(
        sign_string.encode(),
        padding.PKCS1v15(),
        hashes.SHA256()
    )

    return base64.b64encode(signature).decode()

3. 错误处理

当鉴权失败时,被调用方会返回统一格式的错误响应:

{
    "isok": false,
    "msg": "错误描述",
    "code": 错误码,
    "dataObj": null
}

常见错误码说明:

错误码说明
-100签名为空
-101时间戳为空
-102AppId 为空
-104AppId 非法
-103签名验证失败

五、接口列表

1. 封禁特定内容

描述: 对特定内容实行限时整改、封禁、解封等操作

请求方式: POST

请求路径: /ban

请求参数(调用方传入)

参数名类型必填默认值描述
ccidstring封禁专用(全局唯一)ID
casnstringCITV 内部审核单号
actionint-1=立即封禁
1~24=限期(小时)整改
127=解封
codeint处置代码:
0=接口测试
1=涉黄
2=涉暴
3=涉政
4=应监管部门要求
5=应协同部门要求
10=版权纠纷
11=诱导打赏
12=夸大宣传
13=涉老欺诈
14=诈骗嫌疑
15=投诉高发
100=重点防范
messagestringCITV 通知文本

请求示例

{
    "ccid": "cc123456",
    "casn": "ca1234567890",
    "action": -1,
    "code": 4,
    "message": "详情已发送邮件通知到:abc@company.com"
}

响应参数(被调用方返回)

参数名类型描述
ticketIdstring处理追踪编号
ccidstring回传封禁专用 ID
noncestring响应随机串
timestampnumber响应时间戳(Unix 时间戳格式,13 位)
signstring被调用方对 ccid、nonce、ticketId、timestamp(按字母序)的签名

响应示例

{
    "isok": true,
    "msg": "",
    "code": 0,
    "dataObj": {
        "ccid": "cc123456",
        "nonce": "abc123xyz",
        "ticketId": "NzajVXCK6vxsiVyh7y",
        "timestamp": 1630000000000,
        "sign": "Base64EncodedRSASignatureOf_ccid=xxx&nonce=xxx&ticketId=xxx&timestamp=xxx"
    }
}

On this page