Skip to content

签名验签说明

同步数据至企业应用时,需要企业应用对该同步事件进行识别并确认,确保事件来源的安全性和可靠性,从而保证数据在一个安全的环境中进行交互。

签名校验/加解密术语

术语说明
signature消息签名,用于验证请求是否来自IDaaS,以防攻击者伪造。签名算法为HMAC-SHA256 + Base64。
AESKeyAES算法的密钥,加密算法支持AES/GCM/NoPadding和AES/ECB/PKCS5Padding,推荐使用AES/GCM/NoPadding
msg明文消息体,格式为JSON。
encrypt_msg明文消息msg加密处理并进行Base64编码后的密文。

签名校验

为了让企业应用确认事件推送来自IDaaS,IDaaS将事件推送给企业应用回调服务时,请求包体中包含请求签名并以参数signature标识,企业应用需要验证此参数的正确性后再解密,验证步骤如下:

  1. 计算签名:由签名密钥、Nonce值、时间戳、事件类型、加密消息体5部分组成,中间使用&进行连接。采用HMAC-SHA256 + Base64算法进行加密。以下为Java语言签名示例:

    java
    String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec secretKey = new SecretKeySpec(签名秘钥.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    mac.init(secretKey);
    String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
  2. 比较计算的签名cal_signature与请求参数signature是否相等,相等则表示验证通过。

  3. 企业应用按照要求返回响应消息格式。

明文加密过程

  1. 拼接明文字符串。明文字符串由16个字节的随机字符串、明文msg拼接组成,中间使用&进行连接。以下为Java语言示例:

    java
    String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
  2. 对拼接后的明文字符串使用AESkey加密后,再进行Base64编码,获得密文encrypt_msg。以下为Java语言示例,分别演示两种不同的加密算法:

    • AES/ECB/PKCS5Padding:

      java
      Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
      SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(1, secretKey);
      byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
      String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
    • AES/GCM/NoPadding:

      java
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(1, secretKey);
      byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
      //生成长度24的字符串
      String repIvStr = generate(24);
      byte[] iv =  Base64.getDecoder().decode(repIvStr);
      cipher.init(1, secretKey , new GCMParameterSpec(128, iv));
      String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));

密文解密过程

  1. 对密文进行BASE64解码。

    java
    byte[] encryptStr = Base64.getDecoder().decode(data);
  2. 使用AESKey进行解密。

    • AES/ECB/PKCS5Padding:

      java
      Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
      SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(2, secretKey);
      byte[] bytes = cipher.doFinal(encryptStr);
    • AES/GCM/NoPadding:

      java
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
      byte[] iv = Base64.getDecoder().decode(data.substring(0, 24));
      data = data.substring(24);
      cipher.init(2, secretKey, new GCMParameterSpec(AES_KEY_SIZE, iv));
      byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
  3. 获取明文内容。

    • AES/ECB/PKCS5Padding: 需去掉rand_msg头部的16个随机字节,剩余的部分即为明文内容msg

      java
      String dataStr = new String(bytes, CHARSET).substring(17);
    • AES/GCM/NoPadding:

      java
      String dataStr = new String(bytes, CHARSET);

数据签名&加密解密示例

  • 以下为数据签名&加密解密的Java语言示例,分别演示两种算法:

    • AES/ECB/PKCS5Padding:
    java
    
    package BOOT-INF.classes.com.example.demo.controller;
      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import org.apache.commons.lang3.RandomStringUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
    
      import javax.crypto.Cipher;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import javax.servlet.http.HttpServletRequest;
      import java.nio.charset.Charset;
      import java.nio.charset.StandardCharsets;
      import java.util.Base64;
      import java.util.UUID;
    
      @RequestMapping
      @Controller
      public class DemoController {
      private static final Logger log = LoggerFactory.getLogger(DemoController.class);
    
          private static final String SEPARATOR = "&";
    
          private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";
    
          private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";
    
          private static final String TOKEN = "4JVImwu3GdM3zNCE4JVImwu3GdM3zNCE";
    
          private static final String HMAC_SHA256 = "HmacSHA256";
    
          private static final String ENCRYPT_ALGORITHM = "AES/ECB/PKCS5Padding";
    
          private static final String ENCRYPT_KEY_ALGORITHM = "AES";
    
          private static final Charset CHARSET = StandardCharsets.UTF_8;
    
          @ResponseBody
          @RequestMapping({"/callback"})
          public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
              try {
                  // 验证授权信息
                  // 获取 Authorization 头部信息
                  String authorization = httpRequest.getHeader("Authorization");
                  assertAuthorizationMessage(authorization);
    
                  //注:若需判断接收的事件回调消息是否重复,可通过解密后的数据的前16位字符串进行验证。(即&前的字符串用于校验消息是否重复,&后的字符串为对象属性数据)
    
                  JSONObject messageBody = JSON.parseObject(body);
    
                  //校验消息体来源是否合法
                  assertMessageSourceValidity(messageBody);
    
                  //解析加密消息体
                  String encryptMessage = messageBody.getString("data");
                  String plaintMessage = parseEncryptedMessage(encryptMessage);
    
                  // 处理数据
                  JSONObject eventData = JSON.parseObject(plaintMessage);
                  String eventType = messageBody.getString("eventType");
                  JSONObject processResultData = processEventData(eventData, eventType);
    
                  String processResultDataStr = RandomStringUtils.random(16, true, false)+ "&" + processResultData.toJSONString(); ;
                  String encryptResultData =encryptResultData(processResultDataStr);
                  JSONObject result = new JSONObject();
                  result.put("code", "200");
                  result.put("message", "success");
                  result.put("data", encryptResultData);
                  return result;
              } catch (Exception e) {
                  JSONObject result = new JSONObject();
                  result.put("code", "400");
                  String message = e.getMessage();
                  result.put("message", StringUtils.isNotEmpty(message) ? message : e);
                  return result;
              }
          }
    
          private String encryptResultData(String dataStr) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(1, secretKey);
                  byte[] bytes = dataStr.getBytes(CHARSET);
                  return Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
              } catch (Exception e) {
                  throw new RuntimeException("error encrypt Result Data", e);
              }
          }
    
          private void assertAuthorizationMessage(String authorization) {
              if (!"Bearer ".concat(TOKEN).equals(authorization)) {
                  throw new RuntimeException("error authorization.");
              }
          }
    
          private void assertMessageSourceValidity(JSONObject messageBody) {
              try {
                  String nonce = messageBody.getString("nonce");
                  String timestamp = messageBody.getString("timestamp");
                  String eventType = messageBody.getString("eventType");
                  String encryptData = messageBody.getString("data");
                  String signature = messageBody.getString("signature");
    
                  String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
                  Mac mac = Mac.getInstance(HMAC_SHA256);
                  SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
                  mac.init(secretKey);
                  byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
                  String cal_signature = Base64.getEncoder().encodeToString(bytes);
    
                  if (!cal_signature.equals(signature)) {
                      throw new RuntimeException("signature validation failed.");
                  }
              } catch (Exception e) {
                  throw new RuntimeException("error validating message signature", e);
              }
          }
    
          private String parseEncryptedMessage(String encryptMessage) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(2, secretKey);
                  byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMessage));
                  return new String(bytes, CHARSET).substring(17);
              } catch (Exception e) {
                  throw new RuntimeException("error parsing encrypted message", e);
              }
          }
    
          private JSONObject processEventData(JSONObject eventData, String eventType) {
              if (eventType.equals("CREATE_USER")) {
                  return processCreateUser(eventData);
              } else if (eventType.equals("CREATE_ORGANIZATION")) {
                  return processCreateOrganization(eventData);
              } else if (eventType.equals("UPDATE_USER")) {
                  return processUpdateUser(eventData);
              } else if (eventType.equals("UPDATE_ORGANIZATION")) {
                  return processUpdateOrganization(eventData);
              } else if (eventType.equals("DELETE_ORGANIZATION")) {
                  // No data to return
                  processDeleteUser(eventData);
                  return new JSONObject();
              } else if (eventType.equals("DELETE_USER")) {
                  // No data to return
                  processDeleteOrganization(eventData);
                  return new JSONObject();
              } else if (eventType.equals("CHECK_URL")) {
                  return processCheckUrl();
              } else {
                  throw new RuntimeException("the event type is not supported");
              }
          }
    
          private JSONObject processCreateUser(JSONObject eventData) {
              //TODO 业务方处理逻辑
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("username"));
              return returnData;
          }
    
          private JSONObject processCreateOrganization(JSONObject eventData) {
              //TODO 业务方处理逻辑
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("code"));
              return returnData;
          }
    
          private JSONObject processUpdateUser(JSONObject eventData) {
              //TODO 业务方处理逻辑
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("id"));
              return returnData;
          }
    
          private JSONObject processUpdateOrganization(JSONObject eventData) {
              //TODO 业务方处理逻辑
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("id"));
              return returnData;
          }
    
          private void processDeleteUser(JSONObject eventData) {
              //TODO 业务方处理逻辑
          }
    
          private void processDeleteOrganization(JSONObject eventData) {
              //TODO 业务方处理逻辑
          }
    
          private JSONObject processCheckUrl() {
              //TODO 业务方处理逻辑
              JSONObject returnData = new JSONObject(1);
              String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
              returnData.put("randomStr", randomStr);
              return returnData;
          }
      }
    • AES/GCM/NoPadding:
    java
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import org.apache.commons.lang3.RandomStringUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.crypto.Cipher;
    import javax.crypto.Mac;
    import javax.crypto.spec.GCMParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    import javax.servlet.http.HttpServletRequest;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    import java.util.Base64;
    import java.util.UUID;
    
    @RequestMapping
    @Controller
    public class DemoController {
    private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.DemoController.class);
    
        private static final String SEPARATOR = "&";
    
        private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";
    
        private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";
    
        private static final String TOKEN = "4JVImwu3GdM3zNCE";
    
        private static final String HMAC_SHA256 = "HmacSHA256";
    
        private static final String ENCRYPT_ALGORITHM = "AES/GCM/NoPadding";
    
        private static final String ENCRYPT_KEY_ALGORITHM = "AES";
    
        private static final Charset CHARSET = StandardCharsets.UTF_8;
    
        @ResponseBody
        @RequestMapping({"/callback"})
        public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
            try {
                // 验证授权信息
                // 获取 Authorization 头部信息
                String authorization = httpRequest.getHeader("Authorization");
                assertAuthorizationMessage(authorization);
    
                //注:若需判断接收的事件回调消息是否重复,可通过解密后的数据的前16位字符串进行验证。(即&前的字符串用于校验消息是否重复,&后的字符串为对象属性数据)
    
                JSONObject messageBody = JSON.parseObject(body);
    
                //校验消息体来源是否合法
                assertMessageSourceValidity(messageBody);
    
                //解析加密消息体
                String encryptMessage = messageBody.getString("data");
                String plaintMessage = parseEncryptedMessage(encryptMessage);
    
                // 处理数据
                JSONObject eventData = JSON.parseObject(plaintMessage);
                String eventType = messageBody.getString("eventType");
                JSONObject processResultData = processEventData(eventData, eventType);
    
                String encryptResultData = encryptResultData(processResultData);
                JSONObject result = new JSONObject();
                result.put("code", "200");
                result.put("message", "success");
                result.put("data", encryptResultData);
                return result;
            } catch (Exception e) {
                JSONObject result = new JSONObject();
                result.put("code", "400");
                String message = e.getMessage();
                result.put("message", StringUtils.isNotEmpty(message) ? message : e);
                return result;
            }
        }
    
        private String encryptResultData(JSONObject processResultData) {
            try {
                String dataStr = processResultData.toJSONString();
                Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                String ivRandom = RandomStringUtils.random(24, true, true);
                byte[] iv = Base64.getDecoder().decode(ivRandom);
                cipher.init(1, secretKey, new GCMParameterSpec(128, iv));
                byte[] bytes = dataStr.getBytes(CHARSET);
                dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
                return ivRandom + dataStr;
            } catch (Exception e) {
                throw new RuntimeException("error encrypt Result Data", e);
            }
        }
    
        private void assertAuthorizationMessage(String authorization) {
            if (!"Bearer ".concat(TOKEN).equals(authorization)) {
                throw new RuntimeException("error authorization.");
            }
        }
    
        private void assertMessageSourceValidity(JSONObject messageBody) {
            try {
                String nonce = messageBody.getString("nonce");
                String timestamp = messageBody.getString("timestamp");
                String eventType = messageBody.getString("eventType");
                String encryptData = messageBody.getString("data");
                String signature = messageBody.getString("signature");
    
                String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
                Mac mac = Mac.getInstance(HMAC_SHA256);
                SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
                mac.init(secretKey);
                byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
                String cal_signature = Base64.getEncoder().encodeToString(bytes);
    
                if (!cal_signature.equals(signature)) {
                    throw new RuntimeException("signature validation failed.");
                }
            } catch (Exception e) {
                throw new RuntimeException("error validating message signature", e);
            }
        }
    
        private String parseEncryptedMessage(String encryptMessage) {
            try {
                Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                byte[] iv = Base64.getDecoder().decode(encryptMessage.substring(0, 24));
                String encryptMsg = encryptMessage.substring(24);
                cipher.init(2, secretKey, new GCMParameterSpec(128, iv));
                byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMsg));
                return new String(bytes, CHARSET);
            } catch (Exception e) {
                throw new RuntimeException("error parsing encrypted message", e);
            }
        }
    
        private JSONObject processEventData(JSONObject eventData, String eventType) {
            if (eventType.equals("CREATE_USER")) {
                return processCreateUser(eventData);
            } else if (eventType.equals("CREATE_ORGANIZATION")) {
                return processCreateOrganization(eventData);
            } else if (eventType.equals("UPDATE_USER")) {
                return processUpdateUser(eventData);
            } else if (eventType.equals("UPDATE_ORGANIZATION")) {
                return processUpdateOrganization(eventData);
            } else if (eventType.equals("DELETE_ORGANIZATION")) {
                // No data to return
                processDeleteUser(eventData);
                return new JSONObject();
            } else if (eventType.equals("DELETE_USER")) {
                // No data to return
                processDeleteOrganization(eventData);
                return new JSONObject();
            } else if (eventType.equals("CHECK_URL")) {
                return processCheckUrl();
            } else {
                throw new RuntimeException("the event type is not supported");
            }
        }
    
        private JSONObject processCreateUser(JSONObject eventData) {
            //TODO 业务方处理逻辑
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("username"));
            return returnData;
        }
    
        private JSONObject processCreateOrganization(JSONObject eventData) {
            //TODO 业务方处理逻辑
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("code"));
            return returnData;
        }
    
        private JSONObject processUpdateUser(JSONObject eventData) {
            //TODO 业务方处理逻辑
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("id"));
            return returnData;
        }
    
        private JSONObject processUpdateOrganization(JSONObject eventData) {
            //TODO 业务方处理逻辑
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("id"));
            return returnData;
        }
    
        private void processDeleteUser(JSONObject eventData) {
            //TODO 业务方处理逻辑
        }
    
        private void processDeleteOrganization(JSONObject eventData) {
            //TODO 业务方处理逻辑
        }
    
        private JSONObject processCheckUrl() {
            //TODO 业务方处理逻辑
            JSONObject returnData = new JSONObject(1);
            String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
            returnData.put("randomStr", randomStr);
            return returnData;
        }
    }

竹云IDaaS开放平台