Skip to content

接口加密与验签

注解 @MssSafety

定义位置:com.sxpcwlkj.common.annotation.MssSafety,注解目标为方法请求体解密与防重逻辑在 RequestBodyHandlerAdvice 中通过 getMethodAnnotation(MssSafety.class) 读取注解,因此实践中请在具体接口方法上声明 @MssSafety,以确保 decryptRequest / isRepetition 生效;类级仅标注时请与代码行为再核对。

请求体解密、防重、响应加密由 RequestBodyHandlerAdviceResponseResultBodyAdvice 等与 EncryptionPropertiesSysSignService 协同完成(实现以 mms-framework 为准)。

属性说明

属性类型默认值说明
isRepetitionbooleantrue是否开启防重复提交(见 防重幂等
decryptRequestbooleanfalse是否对 请求体 做解密/验签(配合 Encrypt-Type 等)
encryptResponsebooleanfalse是否对 响应体 加密
encryptTypeSafetyTypeEnumAESAES / RSA(枚举:com.sxpcwlkj.common.enums.SafetyTypeEnum

使用示例

java
@MssSafety(decryptRequest = true, encryptResponse = true, encryptType = SafetyTypeEnum.AES)

请求头约定

前端或第三方需在请求头中携带与实现一致的 Encrypt-Type(如 AES / RSA),并与 encryptType 一致;其它头字段(如 App-IdAuthorization)以 RequestBodyHandlerAdvice 与登录态为准。

注意事项

  • 请求体解密流程主要针对 POST / PUT 等带 body 的请求(与历史文档一致)。
  • 前端若开启请求加密,请求头需带 Encrypt-Type,后端对应方法需 @MssSafety(decryptRequest = true)(及正确的 encryptType)。
  • 响应加密开启后,客户端需按约定解密(encryptResponse = true 时由 ResponseResultBodyAdvice 处理)。

配置项(encryption

application.yml 中与注解配套的典型配置如下(数值以当前仓库为准):

yaml
encryption:
  enable: true
  types:
    - AES
    - RSA
  valid-time: 3000        # 与时间戳相关的允许误差,毫秒
  repeated-time: 500      # 防重 Redis 键 TTL,毫秒(示例见主工程 application.yml)

第三方接入 Demo(AES / RSA)

以下示例对应后端使用 @MssSafety(decryptRequest = true) 的接口(例如 POST /v1/openapi/getGameList 等;路径以实际工程为准)。

1) 获取 Token 与密钥

POST /v1/openapi/getToken(无需加签)返回:

  • token(后续放在请求头 Authorization
  • appId(16 位,作为 AES IV)
  • secretKey(AES 密钥,16 位)
  • publicKey(RSA 公钥)

2) 通用请求头

  • Content-Type: application/json
  • Authorization: <token>
  • App-Id: <appId>
  • Encrypt-Type: AESEncrypt-Type: RSA
  • X-Sign: <sign>(也可放在 body 中)
  • X-Timestamp: <timestamp>(也可放在 body 中)

decryptRequest = true 时,X-SignX-Timestamp 可不放在 header,由请求体 ApiSecurityParam 解析并写入 header 供验签使用。

3) AES(JS,适用于浏览器/前端)

签名规则(必须与后端一致):

  1. data 进行参数排序(ASCII 升序)
  2. 过滤空值
  3. 拼接为 key=value&...,并对 key/value 做 encodeURIComponent
  4. 使用 AES/CBC/ZeroPadding 加密该字符串
  5. Base64 输出作为 sign

加签 JS Demo(crypto-js)

typescript
import { AES, enc, mode, pad } from 'crypto-js';

const appId = '16位appId';
const secretKey = '16位secretKey';
const token = '登录接口返回的token';

const data = { pageNum: 1, pageSize: 10, userName: '' };

const paramsSort = (obj: Record<string, any>) => {
  const keys = Object.keys(obj).sort();
  const res: Record<string, any> = {};
  keys.forEach((k) => (res[k] = obj[k]));
  return res;
};

const tansParams = (params: Record<string, any>) => {
  params = paramsSort(params);
  let result = '';
  for (const propName of Object.keys(params)) {
    const value = params[propName];
    const part = encodeURIComponent(propName) + '=';
    if (value !== null && value !== '' && typeof value !== 'undefined') {
      if (typeof value === 'object') {
        for (const key of Object.keys(value)) {
          if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
            const p = propName + '[' + key + ']';
            result += encodeURIComponent(p) + '=' + encodeURIComponent(value[key]) + '&';
          }
        }
      } else {
        result += part + encodeURIComponent(value) + '&';
      }
    }
  }
  return result.endsWith('&') ? result.slice(0, -1) : result;
};

const dataStr = tansParams(data);
const iv = enc.Utf8.parse(appId);
const key = enc.Utf8.parse(secretKey);
const encrypted = AES.encrypt(enc.Utf8.parse(dataStr), key, {
  iv,
  mode: mode.CBC,
  padding: pad.ZeroPadding,
});
const sign = enc.Base64.stringify(encrypted.ciphertext);

const body = {
  appId,
  sign,
  timestamp: Date.now(),
  data,
};

fetch('/v1/openapi/getGameList', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: token,
    'App-Id': appId,
    'Encrypt-Type': 'AES',
  },
  body: JSON.stringify(body),
});

4) RSA(Java / Python / PHP / C#)

签名规则

  1. data 转成 JSON 字符串(保持字段顺序与实际序列化一致)
  2. 使用 公钥 做 RSA 加密(PKCS1 Padding)
  3. 将密文输出为 十六进制字符串 作为 sign

Java Demo(Hutool)

java
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.core.util.HexUtil;
import com.alibaba.fastjson.JSON;

String appId = "16位appId";
String publicKey = "获取Token接口返回的publicKey";
String token = "登录接口返回的token";

Map<String, Object> data = new HashMap<>();
data.put("pageNum", 1);
data.put("pageSize", 10);
data.put("userName", "");

String dataStr = JSON.toJSONString(data);
RSA rsa = new RSA(null, publicKey);
byte[] encrypted = rsa.encrypt(dataStr.getBytes(StandardCharsets.UTF_8), KeyType.PublicKey);
String sign = HexUtil.encodeHexStr(encrypted).toUpperCase();

Map<String, Object> body = new HashMap<>();
body.put("appId", appId);
body.put("sign", sign);
body.put("timestamp", System.currentTimeMillis());
body.put("data", dataStr);

Python Demo(PyCryptodome)

python
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import binascii, json, time

app_id = "16位appId"
public_key = """-----BEGIN PUBLIC KEY-----
...your key...
-----END PUBLIC KEY-----"""

data = {"pageNum": 1, "pageSize": 10, "userName": ""}
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)

key = RSA.import_key(public_key)
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(data_str.encode('utf-8'))
sign = binascii.hexlify(encrypted).decode('utf-8').upper()

body = {
  "appId": app_id,
  "sign": sign,
  "timestamp": int(time.time() * 1000),
  "data": data_str
}

PHP Demo(openssl)

php
<?php
$appId = "16位appId";
$publicKey = "-----BEGIN PUBLIC KEY-----\n...your key...\n-----END PUBLIC KEY-----";

$data = [
  "pageNum" => 1,
  "pageSize" => 10,
  "userName" => ""
];
$dataStr = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

$pubKey = openssl_pkey_get_public($publicKey);
openssl_public_encrypt($dataStr, $encrypted, $pubKey, OPENSSL_PKCS1_PADDING);
$sign = strtoupper(bin2hex($encrypted));

$body = [
  "appId" => $appId,
  "sign" => $sign,
  "timestamp" => round(microtime(true) * 1000),
  "data" => $dataStr
];

C# Demo(RSA)

csharp
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;

string appId = "16位appId";
string publicKeyPem = @"-----BEGIN PUBLIC KEY-----
...your key...
-----END PUBLIC KEY-----";

var data = new Dictionary<string, object> {
  { "pageNum", 1 },
  { "pageSize", 10 },
  { "userName", "" }
};
string dataStr = JsonConvert.SerializeObject(data);

using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem.ToCharArray());
byte[] encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(dataStr), RSAEncryptionPadding.Pkcs1);
string sign = BitConverter.ToString(encrypted).Replace("-", "").ToUpperInvariant();

var body = new Dictionary<string, object> {
  { "appId", appId },
  { "sign", sign },
  { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
  { "data", dataStr }
};

5) 绑定到具体接口

GameApiController#getGameList 为例:

  • 注解:@MssSafety(decryptRequest = true, encryptType = SafetyTypeEnum.RSA)
  • Header:Encrypt-Type: RSA
  • Body:{ appId, sign, timestamp, data }data 为 JSON 字符串或对象,与 ApiSecurityParam 约定一致)

Released under the MIT License.