首批通过分布式安全可靠测评,为关键业务系统打造
API 混合加密
更新时间:2026-04-10 11:58:01
当 obshell 接收到 HTTP 请求时,将对请求进行安全校验。obshell 的校验内容取决于当前节点的身份,包括但不限于密码、URI、请求体(Request Body)的验证。
使用说明
发送请求
当您需要将 HTTP 请求发送到 obshell 时,需按照以下步骤进行操作。
使用 AES 加密算法对请求体(Request Body)内容加密。
生成 16 位的随机 AES 密钥(key)和初始化向量(Initialization Vector,IV)。
使用 CBC 加密模式对序列化后的请求体内容进行加密。
构造用于安全校验的数据结构 HttpHeader,结构体如下:
type HttpHeader struct { Auth string // OceanBase 集群 sys 租户的 root 密码,用于已初始化的集群中节点(或者 MASTER 节点)的安全校验 Ts string // Auth 的有效时间戳 Uri string // 请求的 URI Keys []byte // 加密 HTTP 请求的 Body 时,使用的 AES 加密算法的 key 和 IV }使用 RSA 算法对 HttpHeader 进行加密,并将其设置到 HTTP 请求头部(Request Headers)
调用
/api/v1/secret接口获得目标节点的公钥(该 API 不需要进行安全校验)结合公钥,使用 RSA 算法对序列化后的 HttpHeader 字符串加密,并在加密后进行 Base64 编码,再将其解码成 UTF-8 编码的字符串,得到加密后的字符串 S
设置 HTTP 请求头部 {"X-OCS-Header": S}
obshell 校验流程
obshell Server 在接收到 HTTP 请求后会依次执行以下流程进行安全检验。
从请求头部获得键值对 "X-OCS-Header": S。
结合自身私钥,对 S 进行解密,获得解密后的结构体 HttpHeader。
检查 HttpHeader 中记录的 Auth 是否正确。
检查 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 密码
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)
print("request headers: ", 'X-OCS-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 密码
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,
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
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 密码
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;
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;
}
}