加密字段解密方法
【注意】当加密数据以“Enc.”为前缀时,需通过在线解密接口获取明文数据;若加密数据无此标识,则应采用本地解密方法(旧版本)进行解密。
一、在线加解密
平台提供以下三种核心接口,请根据您的业务场景选择:
批量解密
用于将一批密文解密为原始明文。此接口是核心管控接口,调用会消耗您的每日解密配额。请仅在确实需要完 整明文的场景下使用。
- •请求方法:
POST- •请求路径:
/goodlife/v1/open/common_biz/crypto/decrypt/batch- •批次上限:
50 条/次- •是否计费:是
- •详细介绍:在线批量解密
批量解密后脱敏
强烈推荐!此接口在解密后直接对数据进行脱敏处理,返回部分信息(如“张”、“138***8000”)。它既能满足大部分展示和核对场景的需求,又极大降低了敏感信息暴露的风险。此接口不消耗您的解密配额。
- •请求方法:
POST- •请求路径:
/goodlife/v1/open/common_biz/crypto/decrypt_mask/batch- •批次上限:
50 条/次- •是否计费:否
- •详细介绍:批量解密后脱敏
批量加密
当您需要向平台方安全地回传数据时(例如,在交易回调、信息上报等场景),请使用此接口对明文进行加密。
- •请求方法:
POST- •请求路径:
/goodlife/v1/open/commoncrypto/encrypt/batch- •批次上限:
50 条/次- •是否计费:否
- •详细介绍:批量加密
额度与限流
解密配额
为保障平台数据安全,
/goodlife/v1/open/common_biz/decrypt/batch 接口的调用受到每日动态配额的限制。- •计算单位:配额以
client_key 为维度进行计算,每日更新。- •消耗规则:每次调用
/goodlife/v1/open/common_biz/decrypt/batch 接口,消耗的配额数量等于请求中 cipher_texts 数组的长度。- •每日更新:每日零点,系统会根据您近期的业务指标(如订单量、历史解密成功率等)自动计算并授予新一天的基础配额。
- •临时提额:如遇大型营销活动等可预见的流量高峰,您可以通过开发者后台提前提交临时额度申请。请在申请中说明活动背景、预估解密量级和持续时间。
网关限流
除了解密配额外,所有接口还受到网关层面的通用限流策略约束,以防止恶意攻击和系统过载。
- •QPS 限制:当您在短时间内请求过于频繁,超过了平台设定的阈值,会收到
请求太过频繁,请稍后再试 错误。- •智能计数:对于解密类接口,网关有一项优化策略:同一密文在一小时内被多次请求解密,只会被计入限流值一次。这意味着,如果您因为业务逻辑问题重复解密少量相同密文,对限流的影响会较小。但请注意,这并不改变配额的消耗规则。
- •退避重试:当收到 请求太过频 繁 错误时,请不要立即重试。建议采用指数退避策略(Exponential Backoff),例如,等待 1s, 2s, 4s, ... 后再重试,并设置最大重试次数,避免加剧系统拥堵。
安全与合规
最佳实践:为了最大限度地保护用户数据并符合合规要求,我们强烈建议您优先使用
decrypt_mask/batch(解密后脱敏)接口。仅在业务流程必须完整明文时,才考虑使用 decrypt/batch (解密)接口。- •平台审计:为保障平台数据安全,平台会自动记录所有加解密操作的详细审计日志,定期对加解密行为进行安全审计,提高数据安全防护水平。
- •规范解密:开发者应规范使用解密能力,若商家/技术服务商出现订单信息泄漏、电话骚扰/辱骂、刷单改好评/删差评、线下欺诈等违规行为的,平台将限制或关停其解密接口调用能力。
FAQ
Q1: 何时应该选择
decrypt_mask/batch 接口?A: 在任何不需要获取完整明文进行计算或处理的场景,都应优先选择
decrypt_mask/batch。例如:- •在界面上向客服或运营人员展示用户信息(如 “张*”)。
- •用于非精确的身份核对。
- •记录操作日志时,避免存储完整敏感信息。
使用此接口不仅更安全,而且不消耗宝贵的解密配额。
Q2: 每日的解密配额不够用怎么办?
A: 如果是偶发性不足,请评估业务是否可以优化,更多地使用
decrypt_mask/batch 接口。如果是因大促、活动等原 因导致的临时性需求激增,您可以通过开发者后台的“临时额度申请”通道进行申请。如果是长期性不足,请联系您的业务对接人,平台会根据您的实际业务规模重新评估和调整您的基础配额。Q3: 如果需要处理的密文数量超过了单次请求的批次上限,该如何操作?
A: 您需要对数据进行拆分,分批调用接口。例如,
decrypt/batch 接口的上限是 50,如果您有 120 条密文需要解密,您应该将其拆分为三批(50, 50, 20)分别调用。请在您的代码中实现相应的批处理逻辑。Q4: 遇到“密文与 client_key 不匹配”的错误,该如何定位问题?
A: 这个错误意味着您正在尝试解密一个不属于您的数据。请按以下步骤排查:
- 1.确认该密文的业务来源,它是否是从其他开发者或渠道获取的?
- 2.检查您的
account_id 是否传递正确,是否有可能将 A 用户的密文与 B 用户的 account_id 混淆。- 3.如果问题无法解决,请准备好出错的密文、您使用的
client_key 和 Request ID,联系技术支持。
二、本地解密方法(旧)
- 1.根据ClientKey找到ClientSecret,将ClientSecret向左右使用字符补齐32位/裁剪至32位,补齐:补位字符:#, 先补左侧再补右侧再补左侧······直到补满32位。裁剪:先裁剪左侧再裁右侧再裁左侧······直到剩余32位。(正常不需要补齐,secret默认为32位,此举是为了以防万一)
- 2.将ClientSecret作为Key, 右侧16位为向量IV
- 3.将密文进行base64解码。
- 4.使用AES-256-CBC模式解密解码后的密文,对齐使用PKCS5Padding方式
SDK(旧)
Golang SDK
package utils import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/base64" ) // AesDecrypt 解密函数 // encryptedStr:base64后的密文 // secret:appid/client_key对应的client_secret // return: []byte 明文 func AesDecrypt(encryptedStr string, secret string) ([]byte, error) { // 加密字符串进行base64解码 decodeBytes, err := base64.StdEncoding.DecodeString(encryptedStr) if err != nil { return nil, err } key, iv := parseSecret(secret) block, err := aes.NewCipher(key) if err != nil { return nil, err } blockSize := block.BlockSize() blockMode := cipher.NewCBCDecrypter(block, iv[:blockSize]) origData := make([]byte, len(decodeBytes)) blockMode.CryptBlocks(origData, decodeBytes) origData = PKCS5UnPadding(origData) return origData, nil } // parseSecret 将secret解析为key和iv func parseSecret(secret string) ([]byte, []byte) { // secret对齐为32位 secret = cutSecret(secret) secret = fillSecret(secret) key, iv := secret, secret[16:] return []byte(key), []byte(iv) } func fillSecret(secret string) string { if len(secret) >= 32 { return secret } rightCnt := (32 - len(secret)) / 2 leftCnt := 32 - len(secret) - rightCnt var byt bytes.Buffer byt.Write(bytes.Repeat([]byte("#"), leftCnt)) byt.WriteString(secret) byt.Write(bytes.Repeat([]byte("#"), rightCnt)) return byt.String() } func cutSecret(secret string) string { if len(secret) <= 32 { return secret } rightCnt := (len(secret) - 32) / 2 leftCnt := len(secret) - 32 - rightCnt return secret[leftCnt: 32+leftCnt] } func PKCS5UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] }
Java SDK
package com.douyin.open.goodlife; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class SignUtil { /* * appid/client_key对应的client_secret TODO 这里换成服务商的appsecret */ private static final String secret = "12345678901234566543210987654321"; private static final String key; private static final String iv; static { key = parseSecret(secret); iv =key.substring(16); } /** * @Description AES解密 * @param data base64后的密文 * @return 明文 */ public static String decryptAES(String data) throws Exception { try { byte[] encrypted1 = decode(data);//先用base64解密 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec); byte[] original = cipher.doFinal(encrypted1); String originalString = new String(original); return originalString.trim(); } catch (Exception e) { e.printStackTrace(); return null; } } /** * base64编码 */ public static String encode(byte[] byteArray) { return new String(Base64.getEncoder().encode(byteArray)); } /** * base64解码 */ public static byte[] decode(String base64EncodedString) { return Base64.getDecoder().decode(base64EncodedString); } private static String parseSecret(String secret) { secret = fillSecret(secret); secret = cutSecret(secret); return secret; } private static String cutSecret(String secret) { if (secret.length() <= 32) { return secret; } int rightCnt = (secret.length() - 32) / 2; int leftCnt = secret.length() - 32 - rightCnt; return secret.substring(leftCnt, 32 + leftCnt); } private static String fillSecret(String secret) { if (secret.length() >= 32) { return secret; } int rightCnt = (32 - secret.length()) / 2; int leftCnt = 32 - secret.length() - rightCnt; StringBuilder sb = new StringBuilder(""); for (int i = 0; i < leftCnt; i++) { sb.append('#'); } sb.append(secret); for (int i = 0; i < rightCnt; i++) { sb.append('#'); } return sb.toString(); } }
PHP SDK
<?php class SignUtil { /* * appid/client_key对应的client_secret TODO 这里换成服务商的appsecret */ private static $secret = "12345678901234566543210987654321"; private static $key; private static $iv; public static function init() { self::$key = self::parseSecret(self::$secret); self::$iv = substr(self::$key, 16); } /** * @Description AES解密 * @param string $data base64后的密文 * @return string 明文 */ public static function decryptAES($data) { try { $encrypted = self::decode($data); // 先用base64解密 $decrypted = openssl_decrypt( $encrypted, 'AES-256-CBC', self::$key, OPENSSL_RAW_DATA, self::$iv ); return $decrypted; } catch (Exception $e) { error_log($e->getMessage()); return null; } } /** * base64编码 */ public static function encode($data) { return base64_encode($data); } /** * base64解码 */ public static function decode($data) { return base64_decode($data); } private static function parseSecret($secret) { $secret = self::fillSecret($secret); $secret = self::cutSecret($secret); return $secret; } private static function cutSecret($secret) { if (strlen($secret) <= 32) { return $secret; } $rightCnt = (int)((strlen($secret) - 32) / 2); $leftCnt = strlen($secret) - 32 - $rightCnt; return substr($secret, $leftCnt, 32); } private static function fillSecret($secret) { if (strlen($secret) >= 32) { return $secret; } $rightCnt = (int)((32 - strlen($secret)) / 2); $leftCnt = 32 - strlen($secret) - $rightCnt; $sb = str_repeat('#', $leftCnt); $sb .= $secret; $sb .= str_repeat('#', $rightCnt); return $sb; } } // 初始化静态变量 SignUtil::init(); // 使用示例 $decrypted = SignUtil::decryptAES("加密字符串"); echo $decrypted; ?>
