# 美团企业版签名

# 0. 参考示例

企业接入时,如果使用Java语言,可以参阅sqt-sdk,该代码提供了调用美团企业版接口的示例代码,只需要替换从美团企业版分配的entIdaccessKeysecretKey即可。

# 1. 签名示例

/**
 * @author: chenzhenfeng@meituan.com
 * Date: 17/12/9 下午6:15
 * Description:
 *    示例
 */
public class WaimaiClient {
    private static final String VERSION = "1.0";
    private static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
    private static final String ACCEPT_JSON = "application/json";

    // 测试环境域名
    protected String host = "https://inf-openapi.apigw.test.meituan.com/api/sqt/openapi";
    protected String token;
    protected String entId;
    protected String aesKey;

    public WaimaiClient(String token, Long entId, String aesKey) {
        this.token = token;
        this.entId = entId;
        this.aesKey = aesKey;
    }

    // setter & getter 省略
    private void setBaseParam(BaseApiRequest apiRequest) {
        apiRequest.setTs(System.currentTimeMillis()/1000);
        apiRequest.setEntId(this.entId);
    }

    protected String commonPostInvoke(String url, WmBaseApiRequest request) {
        setBaseParam(request);
        try {
            String rawContent = JsonUtil.object2Json(request, false);
            String body = "token=" + getToken() + "&version=" + VERSION
                    + "&content=" + EncryptUtil.aesEncrypt(rawContent, getAesKey());
            String responseStr = HttpClientUtil.invokePost(url, body, FORM_URLENCODED, ACCEPT_JSON);
            return responseStr;
        } catch (Exception e) {
            LOGGER.error("HTTP调用失败, url:{}", url, e);
        }
        return null;
    }

    public WmPoiListQueryResult poiListQuery(WmPoiListQueryRequest request) {
        request.setMethod("waimai.poi.list");
        String result = commonPostInvoke(host + "/waimai/v1/poi/list", request);
        return JsonUtil.json2Object(result, WmPoiListQueryResult.class);
    }

    public static void main(String[] args) throws Exception {
        // 从美团企业版获取
        WaimaiClient client = new WaimaiClient("xxxx", 1L, "xxxx");

        WmPoiListQueryRequest request = new WmPoiListQueryRequest();
        // 经纬度对应北京望京国际研发园
        request.setLongitude(116488645);
        request.setLatitude(40007069);

        // 查询望京国际研发园附件的外卖商家
        WmPoiListQueryResult result = client.poiListQuery(request);

    }
}

# 2. 请求参数

// 请求基类(公共请求参数)
public class WmBaseApiRequest {
	private Long entId;
    private Long ts;
    private String method;

    // getter & setter 省略
}

// 业务请求示例(商家列表页查询)
public class WmPoiListQueryRequest extends WmBaseApiRequest {
    private Integer longitude;    // 用户当前经度
    private Integer latitude;     // 用户当前纬度
    private String keyword;
    private Integer sort_type;
    private Integer page_index;
    private Integer page_size;

    // getter & setter 省略
}

// 响应基类
public class HttpBaseResult<T> {
    private int status;
    private String msg;
    private T data;
}

// 业务响应体示例 (商家列表页查询结果)
public class PoiListDTO {
    private Integer poi_total_num;
    private Integer have_next_page;
    private Integer current_page_index;
    private Integer page_size;
    private List<PoiBaseInfoDTO> openPoiBaseInfoList;  // 参考接口文档定义
    // getter & setter 省略
}

// 业务响应示例 (商家列表页查询)
public class WmPoiListQueryResult extends HttpBaseResult<PoiListDTO> {
}

# 3. 加密类 EncryptUtil

基于commons-codec实现,pom引入依赖

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.10</version>
</dependency>
/**
 * Author: chenzhenfeng@meituan.com
 * Date: 17/12/9 下午6:15
 * Description:
 */
public class EncryptUtil {
    private EncryptUtil() {

    }
    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptUtil.class);
    private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1";
    private static final String BASE_NUMBER = "0123456789";

    // md5加密
    public static String md5(String plainText) {
        if (StringUtils.isBlank(plainText)) {
            return null;
        } else {
            return DigestUtils.md5Hex(plainText);
        }
    }

    // sha1加密
    public static String sha1(String plainText) {
        if (StringUtils.isBlank(plainText)) {
            return null;
        } else {
            return DigestUtils.sha1Hex(plainText);
        }
    }

    // base64
    public static String base64(String input) {
        return Base64.encodeBase64String(input.getBytes());
    }

    // HMAC_SHA1加密
    public static String hmacSha1(String plainText, String secretKey) {
        SecretKeySpec secretKeySpec = new SecretKeySpec(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(secretKey), ALGORITHM_HMAC_SHA1);
        Mac mac;
        try {
            mac = Mac.getInstance(ALGORITHM_HMAC_SHA1);
            mac.init(secretKeySpec);
            return Base64.encodeBase64String(mac.doFinal(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(plainText)));
        } catch (GeneralSecurityException e) {
            throw new IllegalArgumentException(e);
        }
    }

    // 生成AES秘钥
    public static String generateAESSecretKey() {
        try {
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
            kgen.init(128, new SecureRandom());
            return Base64.encodeBase64String(kgen.generateKey().getEncoded());
        } catch (NoSuchAlgorithmException e) {
            LOGGER.error("AesCypher.genKey NoSuchAlgorithmException", e);
            return null;
        }
    }

    // AES加密
    public static String aesEncrypt(String originText, String secret) throws Exception {
        AesCypher cypher = new AesCypher(secret);
        return cypher.encrypt(originText);
    }

    // AES解密
    public static String aesDecrypt(String encryptedText, String secret) throws Exception{
        AesCypher cypher = new AesCypher(secret);
        return cypher.decrypt(encryptedText);
    }

    public static String getRandomString(int length) {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(BASE_NUMBER.charAt(random.nextInt(BASE_NUMBER.length())));
        }
        return sb.toString();
    }

    // 内部aes类
    static class AesCypher {
        private static final Logger LOGGER = LoggerFactory.getLogger(com.meituan.bep.sqt.lib.common.util.AesCypher.class);

        private static String DEFAULT_SECRET = "hqXj7NwyKYaL_Lc5TlCSzA==";
        private byte[] linebreak;
        private SecretKey key;
        private Cipher cipher;
        private Base64 coder;

        public AesCypher(String secret) {
            this.linebreak = new byte[0];

            try {
                this.coder = new Base64(32, this.linebreak, true);
                byte[] secrets = this.coder.decode(secret);
                // 转换为AES专用密钥
                this.key = new SecretKeySpec(secrets, "AES");
                // 创建密码器,算法/工作模式/补码方式 提供商
                this.cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");
            } catch (Exception e) {
                LOGGER.error("AesCypher.genKey NoSuchAlgorithmException", e);
            }

        }

        public AesCypher() {
            this(DEFAULT_SECRET);
        }

        public synchronized String encrypt(String plainText) throws Exception {
            this.cipher.init(Cipher.ENCRYPT_MODE, this.key);
            byte[] cipherText = this.cipher.doFinal(plainText.getBytes("UTF-8"));
            return new String(this.coder.encode(cipherText));
        }

        public synchronized String decrypt(String codedText) throws Exception {
            byte[] encypted = this.coder.decode(codedText.getBytes());
            this.cipher.init(Cipher.DECRYPT_MODE, this.key);
            byte[] decrypted = this.cipher.doFinal(encypted);
            return new String(decrypted, "UTF-8");
        }
    }

    public static void main(String[] args) throws Exception {
        String plainText = "";
        // aes解密 和 解密
        String aesKey = EncryptUtil.generateAESSecretKey();
        System.out.println("AES秘钥: " + aesKey);
        String aesEncrypted = EncryptUtil.aesEncrypt(plainText, aesKey);
        System.out.println("AES密文: " + aesEncrypted);
        System.out.println("AES明文: " + EncryptUtil.aesDecrypt(aesEncrypted, aesKey));
    }
}

加密结果示例

secreteKey:7URzs0Ee/TAIhPUqlexG/A==
AES加密前明文: {"method":"staff.batch.add","ts":1641264851,"entId":"39597","staffInfos":[{"name":"李杰","entStaffNum":"D0920150228"}]}
AES加密后密文: 38tECwETiBF6vWI30yJIASRJqsQGB4dxcjElrq6cT4L2VwfGlA17jv5gasYObDlc5UYF5Rl2MpRQ44x_wZ9uCnKMlhY21dQHSHrHPfrQ4bwl8SM7uPCiRSy-mXQmI4Ea_DUDHCeSRAX_a05CY-V8vSoQas9PkoBHNJZw7ylxwik

使用上面的secreteKey和加密方法对报文进行加密,如果加密后结果与上述所列不一致,请确认:

  1. 如果不是使用的java编程语言,请确认是否进行了非Java语言额外处理
  2. 采用的加密算法是否与本文档提供的一致

在对接前,可先对以上示例进行测试,确保加密过程执行正确。如访问接口时返回"content不合法"错误信息,请确认能顺利运行出本示例结果。

# 4. Http封装 HttpClientUtil

基于apache httpclient实现,pom引入依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>
/**
 * Author: chenzhenfeng@meituan.com
 * Date: 17/12/11 下午3:25
 * Description:
 */
public class HttpClientUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtil.class);

    private static PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
    private static CloseableHttpClient httpClient = null;
    private static CloseableHttpClient httpsClient = null;
    private static HttpClientUtil.HttpConfig httpConfig = null;
    private static SSLConnectionSocketFactory sslsf = null;
    // 连接池最大连接数和每路由最大连接数可通过httpconf.properties进行配置
    private static final String HTTP_CONF_FILE_NAME = "fenxiao-httpconf.properties";
    private static final int MAX_TOTAL_CONNECTION = 800;
    private static final int MAX_PER_ROUTE = 150;
    private static String UTF_8 = "UTF-8";

    interface ContentType {
        String FORM_URLENCODED = "application/x-www-form-urlencoded";
        String JSON = "application/json; charset=utf-8";
    }

    interface AcceptType {
        String ACCEPT_JSON = "application/json";
        String ACCEPT_ANNY = "*/*";
    }

    private HttpClientUtil() {
        // prevent instantialization
    }

    static {
        loadConf();
        httpClientConnectionManager.setMaxTotal(httpConfig.getMaxTotalConnection());  // 连接池最大连接数
        httpClientConnectionManager.setDefaultMaxPerRoute(httpConfig.getMaxPerRoute());  // 每路由最大连接数

        // 通过连接池获取的httpClient
        httpClient = HttpClients.custom().setConnectionManager(httpClientConnectionManager).build();
        // 通过连接池获取的httpsClient, 能支持https
        httpsClient = HttpClients.custom().setSSLSocketFactory(sslsf).setConnectionManager(httpClientConnectionManager).build();
        LOGGER.info("HttpClient initialization");
    }

    /**
     * 如果配置httpconf.properties, 则从该文件中读取http连接池的配置参数, 否则使用默认值
     */
    private static void loadConf() {
        if (httpConfig == null) {
            httpConfig = new HttpConfig();
        }
        Properties properties = new Properties();
        try {
            InputStream inputStream = HttpClientUtil.class.getClassLoader().getResourceAsStream(HTTP_CONF_FILE_NAME);

            if (inputStream == null) {
                LOGGER.warn("httpConfig file={} does not exist", HTTP_CONF_FILE_NAME);
                return;
            }
            properties.load(inputStream);
            int maxTotalConnection = Integer.parseInt(properties.getProperty("max_total_connection"));
            int maxPerRoute = Integer.parseInt(properties.getProperty("max_per_route"));
            LOGGER.info("max_total_connection={}, max_per_route={}", maxTotalConnection, maxPerRoute);
            httpConfig.setMaxTotalConnection(maxTotalConnection);
            httpConfig.setMaxPerRoute(maxPerRoute);
        } catch (IOException e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        } catch (NumberFormatException e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        } catch (Exception e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        }
    }

    private static void initHttps() {
        X509TrustManager trustManager = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            // 初始化SSL上下文
            sslContext.init(null, new TrustManager[]{trustManager}, null);
            // SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            LOGGER.error("初始化https支持失败", e);
        }
    }

    static class HttpConfig {
        private int maxTotalConnection = MAX_TOTAL_CONNECTION;
        private int maxPerRoute = MAX_PER_ROUTE;

        public int getMaxTotalConnection() {
            return maxTotalConnection;
        }

        public void setMaxTotalConnection(int maxTotalConnection) {
            this.maxTotalConnection = maxTotalConnection;
        }

        public int getMaxPerRoute() {
            return maxPerRoute;
        }

        public void setMaxPerRoute(int maxPerRoute) {
            this.maxPerRoute = maxPerRoute;
        }
    }

    public static String invokePost(String url, Map<String, String> headers, Map<String, String> params) throws Exception {
        URIBuilder uriBuilder = new URIBuilder();
        valueForUriBuilder(url, uriBuilder);

        HttpPost httpPost = new HttpPost(uriBuilder.build());

        if (headers != null && headers.size() > 0) {
            for (Map.Entry<String, String> param : headers.entrySet()) {
                httpPost.addHeader(param.getKey(), param.getValue());
            }
        }

        if (params != null && params.size() > 0) {
            List<NameValuePair> pairs = new ArrayList<NameValuePair>();
            for (Map.Entry<String, String> param : params.entrySet()) {
                pairs.add(new BasicNameValuePair(param.getKey(), param.getValue()));
            }
            httpPost.setEntity(new UrlEncodedFormEntity(pairs, UTF_8));
        }

        return sendRequest(url, httpPost);
    }


    public static String invokePost(String url, Map<String, String> headers, String body) throws Exception {
        URIBuilder uriBuilder = new URIBuilder();
        valueForUriBuilder(url, uriBuilder);

        HttpPost httpPost = new HttpPost(uriBuilder.build());

        if (headers != null && headers.size() > 0) {
            for (Map.Entry<String, String> param : headers.entrySet()) {
                httpPost.addHeader(param.getKey(), param.getValue());
            }
        }

        if (body != null) {
            httpPost.setEntity(new StringEntity(body, Charset.forName(UTF_8)));
        }

        return sendRequest(url, httpPost);
    }

    public static String invokePost(String url, String body, String contentType, String accept) throws Exception {
        URIBuilder uriBuilder = new URIBuilder();
        valueForUriBuilder(url, uriBuilder);

        HttpPost httpPost = new HttpPost(uriBuilder.build());

        httpPost.addHeader("Content-type",contentType);
        httpPost.setHeader("Accept", accept);
        httpPost.setEntity(new StringEntity(body, Charset.forName(UTF_8)));

        return sendRequest(url, httpPost);
    }

    public static String invokeGet(String url) throws Exception {
        // 原始访问url
        HttpGet httpGet = new HttpGet(url);

        return sendRequest(url, httpGet);
    }
    

    private static String sendRequest(String url, HttpRequestBase request) throws Exception {
        CloseableHttpClient client;
        if (url.startsWith("https")) {
            client = httpsClient;
        } else {
            client = httpClient;
        }
        long st = System.currentTimeMillis();
        String responseStr;
        int status = 0;
        try {
            CloseableHttpResponse response = client.execute(request);
            HttpEntity entity = response.getEntity();
            status = response.getStatusLine().getStatusCode();
            if (entity != null) {
                responseStr = EntityUtils.toString(entity, UTF_8);
                response.close();
            } else {
                responseStr = StringUtils.EMPTY;
            }
        } catch (SocketTimeoutException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } catch (ClientProtocolException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } catch (IOException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } finally {
            request.releaseConnection();
        }
        LOGGER.info("HttpClient Success, status:{}, url:{}, use_time:{} ms",
                status, request.getURI(), (System.currentTimeMillis() - st));
        return responseStr;
    }

    private static RequestConfig buildRequestConfig(Integer connTimeout, Integer readTimeout) {
        RequestConfig.Builder customReqConf = RequestConfig.custom();
        if (connTimeout != null) {
            customReqConf.setConnectTimeout(connTimeout);
        }
        if (readTimeout != null) {
            customReqConf.setSocketTimeout(connTimeout);
        }
        return customReqConf.build();
    }

    private static void valueForUriBuilder(String url, URIBuilder uriBuilder) {
        Integer apartIndex = url.indexOf("?");
        if (apartIndex == -1) {
            uriBuilder.setPath(url);
        } else {
            uriBuilder.setPath(url.substring(0, apartIndex));
            uriBuilder.setCustomQuery(url.substring(apartIndex + 1, url.length()));
        }
    }
}

# 5. Node.js版加密解密参考

    const crypto = require('crypto');
    
    var SqtAESUtil = {};
    SqtAESUtil.MyConstanst = {
        ENCODING: 'utf8',
        BASE64: 'base64',
        MODE: 'aes-128-ecb',
        IV: new Buffer(''),
        BUFFER: 'buffer',
        SECRETKEY: 'xd1nzb/N9Nx3+VoImzCsnw=='  //美团企业版提供的测试密钥
    };
    
    
    /**
     * 加密
     * @param plainText  要加密的明文内容
     * @returns {string} 返回字符串
     */
    SqtAESUtil.aesEncrypt = function (plainText) {
        var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
    
        var cipherChunks = [];
        var cipher = crypto.createCipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
        cipher.setAutoPadding(true);
    
        cipherChunks.push(cipher.update(new Buffer(plainText, SqtAESUtil.MyConstanst.ENCODING), SqtAESUtil.MyConstanst.BUFFER, SqtAESUtil.MyConstanst.BASE64));
        cipherChunks.push(cipher.final(SqtAESUtil.MyConstanst.BASE64));
    
        return cipherChunks.join('');
    }
    
    /**
     *  解密
     * @param encryptText 密文
     * @returns {string} 字符串
     */
    SqtAESUtil.aesDecrypt = function (encryptText) {
        var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
    
        var cipherChunks = [];
        var decipher = crypto.createDecipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
        decipher.setAutoPadding(true);
    
        cipherChunks.push(decipher.update(encryptText, SqtAESUtil.MyConstanst.BASE64, SqtAESUtil.MyConstanst.ENCODING));
        cipherChunks.push(decipher.final(SqtAESUtil.MyConstanst.ENCODING));
    
        return cipherChunks.join('');
    }
    
    /**
     * 测试加密解密
     */
    SqtAESUtil.test = function () {
        var data = '{"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}';
        var result = SqtAESUtil.aesEncrypt(data);
    
        console.log("加密之前明文内容: " + data);
        console.log("nodeJs加密结果: " + result);
        console.log("nodeJs解密结果: " + SqtAESUtil.aesDecrypt(result));
    
    
        var sqt = 'UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP-_K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ-5RXHzvMIT0ASH-IXepTP_O16U37QqCkEb5L1WLy4';
        console.log("美团企业版java生成的密文: " + sqt);
        console.log("nodejs解密: " + SqtAESUtil.aesDecrypt(sqt));
    }
    
    // 测试
    SqtAESUtil.test();
    
    // 结果
    /*
        加密之前明文内容: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP+/K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ+5RXHzvMIT0ASH+IXepTP/O16U37QqCkEb5L1WLy4=
        nodeJs加密结果: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP+/K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ+5RXHzvMIT0ASH+IXepTP/O16U37QqCkEb5L1WLy4=
        nodeJs解密结果: {"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}
        美团企业版java生成的密文: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP-_K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ-5RXHzvMIT0ASH-IXepTP_O16U37QqCkEb5L1WLy4
        nodejs解密: {"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}
    */

# 6. C#版加密解密参考

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class EncryptUtil
{
	#region AES加密
    /// <summary>
    /// AES加密
    /// </summary>
    /// <param name="text">明文</param>
    /// <param name="key">密钥,长度为16的字符串</param>
    /// <returns>密文</returns>
    public static string AESEncode(string text, string key)
    {
        byte[] keys = Convert.FromBase64String(key);
        RijndaelManaged rijndaelCipher = new RijndaelManaged();
        rijndaelCipher.Mode = CipherMode.ECB;
        rijndaelCipher.Padding = PaddingMode.PKCS7;
        rijndaelCipher.KeySize = 128;
        rijndaelCipher.BlockSize = 128;
        byte[] pwdBytes = keys;
        byte[] keyBytes = new byte[16];
        int len = pwdBytes.Length;
        if (len > keyBytes.Length)
            len = keyBytes.Length;
        Array.Copy(pwdBytes, keyBytes, len);
        rijndaelCipher.Key = keyBytes;
        ICryptoTransform transform = rijndaelCipher.CreateEncryptor();
        byte[] plainText = Encoding.UTF8.GetBytes(text);
        byte[] cipherBytes = transform.TransformFinalBlock(plainText, 0, plainText.Length);
        return ConvertHelper.ToBase64StringURLSafe(cipherBytes);//输出为Base64
    }
    #endregion

    #region AES解密
    /// <summary>
    /// AES解密
    /// </summary>
    /// <param name="text">密文</param>
    /// <param name="key">密钥,长度为16的字符串</param>
    /// <returns>明文</returns>
    public static string AESDecode(string text, string key)
    {
        RijndaelManaged rijndaelCipher = new RijndaelManaged();
        rijndaelCipher.Mode = CipherMode.ECB;
        rijndaelCipher.Padding = PaddingMode.PKCS7;
        rijndaelCipher.KeySize = 128;
        rijndaelCipher.BlockSize = 128;
        byte[] encryptedData = ConvertHelper.FromBase64StringURLSafe(text);
        byte[] pwdBytes = Convert.FromBase64String(key);
        byte[] keyBytes = new byte[16];
        int len = pwdBytes.Length;
        if (len > keyBytes.Length)
            len = keyBytes.Length;
        Array.Copy(pwdBytes, keyBytes, len);
        rijndaelCipher.Key = keyBytes;
        ICryptoTransform transform = rijndaelCipher.CreateDecryptor();
        byte[] plainText = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
        return Encoding.UTF8.GetString(plainText);
    }
    #endregion
}

非Java语言额外处理

需要注意的是,美团企业版基于Java实现,标准的加解密格式是urlSafe的Base64编码,该格式会在基础Base64编码的之上,额外处理'+'、'/'、'='。这里我们也提供一个相应的处理工具类。

public class ConvertHelper
{
    /// <summary>
    /// 将Java安全的base64字符串转换为byte数组
    /// </summary>
    /// <param name="convert"></param>
    /// <param name="javaURLSafeString"></param>
    /// <returns></returns>
    public static byte[] FromBase64StringURLSafe(string javaURLSafeString)
    {
        javaURLSafeString = javaURLSafeString.Replace("-", "+").Replace("_", "/");
        var base64 = Encoding.ASCII.GetBytes(javaURLSafeString);
        var padding = base64.Length * 3 % 4;
        if (padding != 0)
        {
            javaURLSafeString = javaURLSafeString.PadRight(javaURLSafeString.Length + padding, '=');
        }
        return Convert.FromBase64String(javaURLSafeString);
    }

    /// <summary>
    /// 将byte数组转换为java安全的base64字符串
    /// </summary>
    /// <param name="convert"></param>
    /// <param name="bytes"></param>
    /// <returns></returns>
    public static string ToBase64StringURLSafe(byte[] bytes)
    {
        string base64String = Convert.ToBase64String(bytes);
        return base64String.Replace("+", "-")
            .Replace("/", "_")
            .Replace("=", "");
    }
}
上次更新: 4/22/2024, 4:15:08 PM