员工身份(EIAM)

签名验签说明

身份同步

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

# 签名校验/加解密术语

术语 说明
signature 消息签名,用于验证请求是否来自IDaaS,以防攻击者伪造。签名算法为HMAC-SHA256 + Base64。
AESKey AES算法的密钥,加密算法支持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语言签名示例:

    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)));
    
    1
    2
    3
    4
    5
  2. 比较计算的签名cal_signature与请求参数signature是否相等,相等则表示验证通过。

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

# 明文加密过程

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

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

    • AES/ECB/PKCS5Padding:

      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));
      
      1
      2
      3
      4
      5
    • AES/GCM/NoPadding:

      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
      2
      3
      4
      5
      6
      7
      8
      9

# 密文解密过程

  1. 对密文进行BASE64解码。

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

    • AES/ECB/PKCS5Padding:

      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);
      
      1
      2
      3
      4
    • AES/GCM/NoPadding:

      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));
      
      1
      2
      3
      4
      5
      6
  3. 获取明文内容。

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

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

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

# 数据签名&加密解密示例

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

    • AES/ECB/PKCS5Padding:
    
    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;
          }
      }
    
    
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    • AES/GCM/NoPadding:
    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;
        }
    }
    
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211