首批通过分布式安全可靠测评,为关键业务系统打造
API 混合加密
更新时间:2026-04-07 20:57:20
当 obshell 接收到 HTTP 请求时,将对请求进行安全校验。obshell 的校验内容取决于当前节点的身份,包括但不限于密码、URI、请求体(Request Body)的验证。
使用说明
发送请求
当您需要将 HTTP 请求发送到 obshell 时,需按照以下步骤进行操作。
使用 AES 加密算法对请求体(Request Body)内容加密。
生成 16 位的随机 AES 密钥(key)和初始化向量(Initialization Vector,IV)。
使用 CBC 加密模式对序列化后的请求体内容进行加密。
构造用于安全校验的数据结构 HttpHeader,结构体如下。
type HttpHeader struct { Auth string Ts string // Auth 的有效时间戳 Uri string // 请求的 URI Keys []byte // 加密 HTTP 请求的 Body 时,使用的 AES 加密算法的 key 和 IV }其中,赋值的密码类型(
Auth)不同,HTTP 请求校验的通过情况也不同:当Auth被设置为 obshell 节点密码时,能够通过所有组件相关的 HTTP 请求校验;而当Auth被设置为 root@sys 用户密码时,只能通过除 OBProxy 外其他组件的 HTTP 请求校验(如部署 OceanBase 数据库和 Agent、集群运维相关请求等)。为 obshell 节点设置密码的/api/v1/agent/password接口的请求校验方式也比较特殊,具体介绍可参见 设置节点密码。使用 RSA 算法对 HttpHeader 进行加密,并将其设置到 HTTP 请求头部(Request Headers)。
调用
/api/v1/secret接口获得目标节点的公钥(该 API 不需要进行安全校验)结合公钥,使用 RSA 算法对序列化后的 HttpHeader 字符串加密,并在加密后进行 Base64 编码,再将其解码成 UTF-8 编码的字符串,得到加密后的字符串 S
设置 HTTP 请求头部
- 如果
Auth设置为 obshell 节点密码,则设置请求头部:{"X-OCS-Agent-Header": S} - 如果
Auth设置为 root@sys 密码,则设置请求头部为:{"X-OCS-Header": S}
- 如果
obshell 校验流程
obshell Server 在接收到 HTTP 请求后会依次执行以下流程进行安全检验。
从请求头部获得键值对
"X-OCS-Header": S或者"X-OCS-Agent-Header": S。结合自身私钥,对 S 进行解密,获得解密后的结构体 HttpHeader。
检查 HttpHeader 中记录的 Auth 是否正确,如果
Auth和请求 API 所属的组件不匹配,直接返回相应的错误提示。检查 HttpHeader 中记录的 Uri 与实际请求的 Uri 是否一致。
结合 HttpHeader 中记录的 Keys 对请求体内容进行解密,得到请求体明文。若解密失败,则验证失败。
只有通过以上安全校验步骤,obshell Server 才会继续对请求进行处理。否则,将向请求方响应安全校验失败。
Python 发送请求代码
以下为使用 Python 发送请求的代码示例,你只需要修改代码中定义的 ip、port、pwd、uri 和 body(含义见代码注释)。修改后编译执行该代码,即可得到对应的 HTTP 请求头部和 HTTP 请求体。
说明
执行如下 Python 脚本需使用 3.5 或之后版本的 Python。
import requests as req
import json
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
from datetime import datetime
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
aes_key = get_random_bytes(16)
aes_iv = get_random_bytes(16)
max_chunk_size = 53
def encrypt(s, pk):
key = RSA.import_key(base64.b64decode(pk))
cipher = PKCS1_cipher.new(key)
data_to_encrypt = bytes(s.encode('utf8'))
chunks = [data_to_encrypt[i:i + max_chunk_size] for i in range(0, len(data_to_encrypt), max_chunk_size)]
encrypted_chunks = [cipher.encrypt(chunk) for chunk in chunks]
encrypted = b''.join(encrypted_chunks)
encoded_encrypted_chunks = base64.b64encode(encrypted).decode('utf-8')
return encoded_encrypted_chunks
def encrypt_body(body, pk):
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
return base64.b64encode(cipher.encrypt(pad(bytes(body.encode('utf8')), AES.block_size))).decode('utf8')
class Header:
auth: str
ts: str
uri: str
keys: bytes
def __init__(self, auth, ts, uri, keys):
self.auth = auth
self.ts = ts
self.uri = uri
self.keys = keys
def serialize_struct(struct):
return json.dumps({
'auth': struct.auth,
'ts': struct.ts,
'uri': struct.uri,
'keys': base64.b64encode(struct.keys).decode('utf-8')
})
ip = '10.10.10.1' # 请求节点的 IP
port = 2886 # 请求节点的服务端口
pwd = '******' # OceanBase 集群 sys 租户的 root 密码
agent_pwd = '******' # obshell 节点密码
uri = '/api/v1/ob/init' # 请求的 URI
body = {} # Request Body 在此更新
# 获取公钥
resp = req.get(f'http://{ip}:{port}/api/v1/secret').text
resp = json.loads(resp)
pk = resp['data']['public_key']
header = Header(pwd, str(int(datetime.now().timestamp()) + 100000), uri, aes_key + aes_iv)
# X-OCS-Agent-Header
# header = Header(agent_pwd, str(int(datetime.now().timestamp()) + 100000), uri, aes_key + aes_iv)
print("request headers: ", 'X-OCS-Header: ' + encrypt(serialize_struct(header), pk))
# X-OCS-Agent-Header
# print("request headers: ", 'X-OCS-Agent-Header: ' + encrypt(serialize_struct(header), pk))
print("\n")
print("request body: ", encrypt_body(json.dumps(body), pk))
print("\n")
print("encrypt password: ", encrypt(pwd, pk))
Go 发送请求代码
以下为使用 Go 发送请求的代码示例,你只需要修改代码中定义的 ip、port、pwd、uri 和 body(含义见代码注释)。修改后编译执行该代码,即可得到对应的 HTTP 请求头部和 HTTP 请求体。
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
var (
ip = "10.10.10.1" // 请求节点的 IP
port = 2886 // 请求节点的服务端口
uri = "/api/v1/ob/init" // 请求的 URI
pwd = "******" // OceanBase 集群 sys 租户的 root 密码
// agentPwd = "********" // obshell 节点密码
body = map[string]interface{}{} // Request Body 在此更新
)
var pk string
func main() {
var err error
pk, err = getPublicKey(fmt.Sprintf("http://%s:%d/api/v1/secret", ip, port))
if err != nil {
fmt.Printf("get public key failed: %s", err.Error())
return
}
fmt.Printf("target agent's public key: %s\n\n", pk)
encryptedBody, header, err := BuildBodyAndHeader(uri, body)
if err != nil {
return
}
fmt.Printf("request header: %v\n\nrequest body: %v\n", header, encryptedBody)
}
// getPublicKey function retrieves the public key from the API
func getPublicKey(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var response struct {
Data struct {
PublicKey string `json:"public_key"`
} `json:"data"`
}
if err = json.Unmarshal(body, &response); err != nil {
return "", err
}
return response.Data.PublicKey, nil
}
func BuildBodyAndHeader(uri string, param interface{}) (encryptedBody interface{}, header map[string]string, err error) {
encryptedBody, Key, Iv, err := EncryptBodyWithAes(param)
if err != nil {
return nil, nil, err
}
header = BuildHeader(Key, Iv)
return encryptedBody, header, nil
}
type HttpHeader struct {
Auth string
Ts string
Uri string
Keys []byte
}
func BuildHeader(keys ...[]byte) map[string]string {
headers := make(map[string]string)
var aesKeys []byte
if len(keys) != 2 {
aesKeys = nil
} else {
aesKeys = append(keys[0], keys[1]...)
}
header := HttpHeader{
Auth: pwd,
// X-OCS-Agent-Header
// Auth: agentPwd,
Ts: fmt.Sprintf("%d", time.Now().Add(100*time.Second).Unix()),
Uri: uri,
Keys: aesKeys,
}
mAuth, err := json.Marshal(header)
if err != nil {
return nil
}
encrypt, err := RSAEncrypt(mAuth, pk)
if err != nil {
return nil
}
headers["X-OCS-Header"] = encrypt
// X-OCS-Agent-Header
// headers["X-OCS-Agent-Header"] = encrypt
return headers
}
func RSAEncrypt(raw []byte, pk string) (string, error) {
pkix, err := base64.StdEncoding.DecodeString(pk)
if err != nil {
return "", err
}
pub, err := x509.ParsePKCS1PublicKey(pkix)
if err != nil {
return "", err
}
if len(raw) == 0 {
b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, raw)
return base64.StdEncoding.EncodeToString(b), err
}
// 分段加密
blockSize := 512/8 - 11
numBlocks := (len(raw) + blockSize - 1) / blockSize
ciphertext := make([]byte, 0)
for i := 0; i < numBlocks; i++ {
start := i * blockSize
end := (i + 1) * blockSize
if end > len(raw) {
end = len(raw)
}
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, pub, raw[start:end])
if err != nil {
return "", err
}
ciphertext = append(ciphertext, encrypted...)
}
return base64.StdEncoding.EncodeToString(ciphertext), err
}
func EncryptBodyWithAes(body interface{}) (encryptedBody interface{}, key []byte, iv []byte, err error) {
if body == nil {
return
}
mBody, err := json.Marshal(body)
if err != nil {
return
}
key = make([]byte, 16)
iv = make([]byte, 16) // equal to block_size,16 bytes
_, err = rand.Read(key)
if err != nil {
return
}
_, err = rand.Read(iv)
if err != nil {
return
}
encryptedBody, err = AESEncrypt(mBody, key, iv)
return
}
func AESEncrypt(raw []byte, key []byte, iv []byte) (string, error) {
// 创建 AES 加密器
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
raw = pkcs5Padding(raw, block.BlockSize())
// 创建 AES 的 CBC 模式加密器
mode := cipher.NewCBCEncrypter(block, iv)
// 加密数据
ciphertext := make([]byte, len(raw))
mode.CryptBlocks(ciphertext, raw)
return base64.StdEncoding.EncodeToString(ciphertext), err
}
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
Java 示例代码
以下为使用 Java 发送请求的代码示例,你只需要修改代码中定义的 agent、pwd、uri 和 body(含义见代码注释)。修改后编译执行该代码,即可得到对应的 HTTP 请求头部和 HTTP 请求体。
package com.oceanbase;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.net.URL;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.gson.Gson;
public class Main {
/* 在这里修改密码、uri 和目标 agent */
private static final String pwd = "******"; //OceanBase 集群 sys 租户的 root 密码
// X-OCS-Agent-Header
// private static final String agentPwd = "******"; // obshell 节点密码
private static final String uri = "/api/v1/obcluster/config"; // 请求的 URI
private static final String agent = "10.10.10.1:2886"; // 请求节点的 IP 和端口
private static final String RSA_ALGORITHM = "RSA";
private static final Integer RSA_KEY_SIZE = 512;
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_ALGORITHM = "AES";
private static final Integer AES_KEY_SIZE = 128;
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
public static class HTTPHeader implements Serializable {
private static final long serialVersionUID = 1L;
public String auth;
public String ts;
public String uri;
private String keys;
public HTTPHeader(String pwd, int timestamp, String uri, byte[] keys) {
this.auth = pwd;
// X-OCS-Agent-Header
// this.auth = agentPwd;
this.ts = String.valueOf(timestamp);
this.uri = uri;
this.keys = Base64.getEncoder().encodeToString(keys);
}
}
public static String serializeStruct(HTTPHeader header) {
Gson gson = new Gson();
// 将结构转化为 JSON 并对 keys 字段进行 Base64 编码
return gson.toJson(header);
}
public static String encryptRSAWithSegment(Cipher cipher, byte[] data) throws Exception {
int inputLen = data.length;
int maxSize = RSA_KEY_SIZE / 8 - 11; // 数据长度减去加密填充
int offSet = 0;
byte[] cache;
int i = 0;
List<byte[]> encryptedSegments = new ArrayList<>();
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > maxSize) {
cache = cipher.doFinal(data, offSet, maxSize);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
encryptedSegments.add(cache);
i++;
offSet = i * maxSize;
}
// 合并加密的段
int totalLen = encryptedSegments.stream().mapToInt(segment -> segment.length).sum();
byte[] encryptedData = new byte[totalLen];
int currentIndex = 0;
for (byte[] segment : encryptedSegments) {
System.arraycopy(segment, 0, encryptedData, currentIndex, segment.length);
currentIndex += segment.length;
}
return Base64.getEncoder().encodeToString(encryptedData);
}
private static String getPublicKey(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
System.out.println("Response: " + response.toString());
JSONObject jsonResponse = JSON.parseObject(response.toString());
System.out.println("Public Key: " + jsonResponse.getJSONObject("data").getString("public_key"));
return jsonResponse.getJSONObject("data").getString("public_key");
} catch (Exception e){
System.out.println("Failed to read response: " + e.getMessage());
throw new Exception("Failed to read response", e);
}
} else {
throw new Exception("HTTP request failed with code " + responseCode);
}
}
private static PublicKey convertPKCS1ToPublicKey(String pkcs1PublicKeyStr) throws Exception {
pkcs1PublicKeyStr = pkcs1PublicKeyStr.replaceAll("\\n", "")
.replace("-----BEGIN RSA PUBLIC KEY-----", "")
.replace("-----END RSA PUBLIC KEY-----", "");
byte[] pkcs1PublicKey = Base64.getDecoder().decode(pkcs1PublicKeyStr);
ASN1InputStream asn1InputStream = new ASN1InputStream(pkcs1PublicKey);
ASN1Sequence sequence = (ASN1Sequence) asn1InputStream.readObject();
BigInteger modulus = ((ASN1Integer) sequence.getObjectAt(0)).getValue();
BigInteger publicExponent = ((ASN1Integer) sequence.getObjectAt(1)).getValue();
asn1InputStream.close();
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(publicKeySpec);
}
public static void main(String[] args) throws Exception {
// 获取公钥
String publicKeyStr = getPublicKey("http://" +agent+ "/api/v1/secret");
PublicKey publicKey = convertPKCS1ToPublicKey(publicKeyStr);
// 初始化 RSA 和 AES 密钥
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
keyGen.initialize(RSA_KEY_SIZE); // RSA 2048 位密钥
KeyGenerator aesKeyGen = KeyGenerator.getInstance(AES_ALGORITHM);
aesKeyGen.init(AES_KEY_SIZE); // AES 128 位密钥
SecretKey aesKey = aesKeyGen.generateKey();
byte[] aesKeyBytes = aesKey.getEncoded();
byte[] iv = new byte[16]; // 初始化向量为 16 字节
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// RSA 分段加密数据
Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION);
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 准备 Header 中要存放的数据 (URI, 时间戳, AES 密钥和 IV)
// 将数据以 Base64 形式编码,然后拼接成一个字符串
long timestamp = System.currentTimeMillis() / 1000 + 100000;
HTTPHeader header = new HTTPHeader(pwd, (int) timestamp, uri, concatenate(aesKeyBytes, iv));
System.out.println("Header: " + serializeStruct(header));
String encryptedHeader = encryptRSAWithSegment(rsaCipher, serializeStruct(header).getBytes(StandardCharsets.UTF_8));
// AES 加密 HTTP 数据体
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORMATION);
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
/* 在这里构建 Request Body */
JSONObject databody = new JSONObject();
data.body.put("clusterId", 1);
data.body.put("clusterName", "21421");
data.body.put("rootPwd", "2145325");
System.out.println(data.body.toString());
byte[] encryptedBody = aesCipher.doFinal(data.body.toString().getBytes(StandardCharsets.UTF_8));
String encryptedBodyBase64 = Base64.getEncoder().encodeToString(encryptedBody);
// 输出结果
System.out.println("Encrypted Header (Base64, RSA-Encrypted): " + encryptedHeader);
System.out.println("Encrypted Body (Base64, AES-Encrypted): " + encryptedBodyBase64);
}
private static byte[] concatenate(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int currentIndex = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, currentIndex, array.length);
currentIndex += array.length;
}
return result;
}
}