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 密钥对的生成与交换:
- 调用方(CITV):自行生成 RSA 密钥对,将公钥提供给 SaaS 平台
- 被调用方(SaaS 平台):自行生成 RSA 密钥对,将公钥提供给 CITV
- 双方均使用对方公钥验证签名,使用自己私钥进行签名
- 密钥长度要求: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 分配 appid | SaaS | SaaS 为 CITV 分配唯一应用标识 appid |
| 对接准备 | SaaS 提供接口地址 | SaaS | 提供 /ban/v2 接口的完整访问 URL |
| 请求阶段 | 构造请求参数 | CITV | 构造 ccid, casn, action, code 等业务参数 |
| 请求阶段 | 生成请求签名 | CITV | 对 appid, casn, ccid, nonce, timestamp 按字母序签名 |
| 请求阶段 | 发送请求 | CITV | POST 请求,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 位) |
| nonce | 是 | String 防重放随机串,推荐使用 UUID |
| appid | 是 | 应用唯一标识,由被调用方分配 |
调用方签名生成方式
签名范围:appid、casn、ccid、nonce、timestamp
签名流程:
- 按字母序拼接签名原文:
appid={appid}&casn={casn}&ccid={ccid}&nonce={nonce}×tamp={timestamp} - 使用 SHA256withRSA 算法,用调用方 RSA 私钥对签名原文进行签名
- 将签名结果进行 Base64 编码,即为
sign值
待签名字符串格式示例:
appid=your_app_id&casn=ca1234567890&ccid=cc123456&nonce=abc123xyz×tamp=1630000000000Go 签名示例
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×tamp=%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 平台)处理规则
请求签名验证
被调用方收到请求后:
- 使用相同规则拼接待签名字符串
- 使用调用方(CITV)RSA 公钥验证签名有效性
- 检查时间戳是否在有效期内(建议 5 分钟)
- 验证 nonce 唯一性防止重放攻击
响应签名规则
被调用方在响应时,需对 ccid、nonce、ticketId、timestamp 字段按字母序进行签名返回。
响应签名流程:
- 按字母序拼接签名原文:
ccid={ccid}&nonce={nonce}&ticketId={ticketId}×tamp={timestamp} - 使用被调用方 RSA 私钥进行 SHA256WithRSA 签名
- Base64 编码后放入响应的
sign字段
待签名字符串格式示例:
ccid=cc123456&nonce=abc123xyz&ticketId=NzajVXCK6vxsiVyh7y×tamp=1630000000000TypeScript 响应签名示例
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}×tamp=${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×tamp=%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×tamp=%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}×tamp={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 | 时间戳为空 |
| -102 | AppId 为空 |
| -104 | AppId 非法 |
| -103 | 签名验证失败 |
五、接口列表
1. 封禁特定内容
描述: 对特定内容实行限时整改、封禁、解封等操作
请求方式: POST
请求路径: /ban
请求参数(调用方传入)
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
| ccid | string | 是 | 封禁专用(全局唯一)ID | |
| casn | string | 是 | CITV 内部审核单号 | |
| action | int | 是 | -1=立即封禁1~24=限期(小时)整改127=解封 | |
| code | int | 是 | 处置代码:0=接口测试1=涉黄2=涉暴3=涉政4=应监管部门要求5=应协同部门要求10=版权纠纷11=诱导打赏12=夸大宣传13=涉老欺诈14=诈骗嫌疑15=投诉高发100=重点防范 | |
| message | string | 否 | CITV 通知文本 |
请求示例
{
"ccid": "cc123456",
"casn": "ca1234567890",
"action": -1,
"code": 4,
"message": "详情已发送邮件通知到:abc@company.com"
}响应参数(被调用方返回)
| 参数名 | 类型 | 描述 |
|---|---|---|
| ticketId | string | 处理追踪编号 |
| ccid | string | 回传封禁专用 ID |
| nonce | string | 响应随机串 |
| timestamp | number | 响应时间戳(Unix 时间戳格式,13 位) |
| sign | string | 被调用方对 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×tamp=xxx"
}
}