
# 签名验签说明
当OneAccess同步数据至企业应用时，需要企业应用对该同步事件进行识别并确认，确保事件来源的安全性和可靠性，从而保证数据在一个安全的环境中进行交互。
#### 签名校验/加解密术语
表1术语 
| 术语           | 说明                                                          |
|:---|:---|
| signature    | 消息签名，用于验证请求是否来自OneAccess，以防攻击者伪造。签名算法为HMAC-SHA256 + Base64。 |
| AESKey       | AES算法的密钥，加密算法为AES/GCM/NoPadding + Base64。                   |
| msg          | 明文消息体，格式为JSON。                                              |
| encrypt _msg | 明文消息msg加密处理并进行Base64编码后的密文。                                 |
   
#### 签名校验
为了让企业应用确认事件推送来自OneAccess，OneAccess将事件推送给企业应用回调服务时，请求body体中包含请求签名并以参数signature标识，企业应用需要验证此参数的正确性后再解密，验证步骤如下：
1. 计算签名。
   计算签名由签名密钥、Nonce值（nonce）、时间戳（timestamp）、事件类型（eventType）、消息体（data）5部分组成，中间使用\&进行连接。采用HMAC-SHA256 + Base64算法进行加密。以下为Java语言签名示例：
   ```
   String message = nonce + "&" + timestamp + "&" + eventType + "&" + data; 
   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语言示例：
   ```
   String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
   ```
   
2. 对拼接后的明文字符串使用AESkey加密后，再进行Base64编码，获得密文encrypt_msg。以下为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); 
   String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
   ```
   
 
#### 密文解密过程
1. 对密文进行BASE64解码。
   ```
   byte[] encryptStr = Base64.getDecoder().decode(data);
   ```
   
2. 使用AESKey进行解密。
   ```
   Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 
   SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); 
   cipher.init(2, secretKey); 
   byte[] bytes = cipher.doFinal(encryptStr);
   ```
   
3. 去掉rand_msg头部的16个随机字节，剩余的部分即为明文内容msg。
   ```
   String dataStr = StringUtils.split(new String(bytes, StandardCharsets.UTF_8), "&")[1];
   ```
   
 
#### 数据签名\&加密解密示例
- 以下为数据签名\& AES/GCM/NoPadding算法加密解密的Java语言示例：
  ```
  package com.example.demo.controller;
  import com.alibaba.fastjson.JSONObject;
  import java.nio.charset.Charset;
  import java.nio.charset.StandardCharsets;
  import java.util.Base64;
  import javax.crypto.Cipher;
  import javax.crypto.Mac;
  import javax.crypto.spec.GCMParameterSpec;
  import javax.crypto.spec.SecretKeySpec;
  import javax.servlet.http.HttpServletRequest;
  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.util.Assert;
  import org.springframework.web.bind.annotation.RequestBody;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.bind.annotation.ResponseBody;
  @RequestMapping
  @Controller
  public class GcmController {
      private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.GcmController.class);
      //region 以下三个参数均由第三方随机生成的32位字符串(英文字母大小写和数字)，正式环境不要使用以下示例
      /**
       * 安全令牌
       */
      private static String token;
      /**
       * 签名密钥
       */
      private static String signKey;
      /**
       * 加密密钥
       */
      private static String encryptionKey;
      //endregion
      /**
       * 加密算法
       */
      private static String hmacSha256;
      private static String encryptAlgorithm;
      private static String encryptKeyAlgorithm;
      private static int aesKeySize;
      private static final Charset CHARSET = StandardCharsets.UTF_8;
      public static void setToken(String token) {
          GcmController.token = token;
      }
      public static void setSignKey(String signKey) {
          GcmController.signKey = signKey;
      }
      public static void setEncryptionKey(String encryptionKey) {
          GcmController.encryptionKey = encryptionKey;
      }
      public static void setHmacSha256(String hmacSha256) {
          GcmController.hmacSha256 = hmacSha256;
      }
      public static void setEncryptAlgorithm(String encryptAlgorithm) {
          GcmController.encryptAlgorithm = encryptAlgorithm;
      }
      public static void setEncryptKeyAlgorithm(String encryptKeyAlgorithm) {
          GcmController.encryptKeyAlgorithm = encryptKeyAlgorithm;
      }
      public static void setAesKeySize(int aesKeySize) {
          GcmController.aesKeySize = aesKeySize;
      }
      public static String getToken() {
          return token;
      }
      public static String getSignKey() {
          return signKey;
      }
      public static String getEncryptionKey() {
          return encryptionKey;
      }
      public static String getHmacSha256() {
          return hmacSha256;
      }
      public static String getEncryptAlgorithm() {
          return encryptAlgorithm;
      }
      public static String getEncryptKeyAlgorithm() {
          return encryptKeyAlgorithm;
      }
      public static int getAesKeySize() {
          return aesKeySize;
      }
      @ResponseBody
      @RequestMapping({"/gcm/callback"})
      public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
          log.info("接受事件回调信息数据，原始报文：" + body);
          JSONObject result = new JSONObject();
          result.put("code", "200");
          result.put("message", "success");
          String authorization = httpRequest.getHeader("Authorization");
          if (!StringUtils.join((Object[]) new String[]{"Bearer ", token}).equals(authorization)) {
              result.put("code", "401");
              result.put("message", "不合法的请求！");
              printResult(result);
              return result;
          }
          JSONObject request = JSONObject.parseObject(body);
          String signature = request.getString("signature");
          String eventType = request.getString("eventType");
          log.info("事件类型" + eventType);
          String data = request.getString("data");
          if (StringUtils.isNotEmpty(signKey))
              try {
                  log.info("开始校验签名");
                  String message = request.getString("nonce") + "&" + request.getLong("timestamp") + "&" + eventType + "&" + data;
                  Mac mac = Mac.getInstance(hmacSha256);
                  SecretKeySpec secretKey = new SecretKeySpec(signKey.getBytes(CHARSET), hmacSha256);
                  mac.init(secretKey);
                  String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(CHARSET)));
                  Assert.isTrue(newSignature.equals(signature), "签名不一致");
                  log.info("签名校验成功");
              } catch (Exception e) {
                  log.error("签名校验失败", e);
                  result.put("code", "401");
                  result.put("message", "签名校验失败");
                  printResult(result);
                  return result;
              }
          if (StringUtils.isNotEmpty(encryptionKey)) {
              log.info("开始解密数据" + data);
              try {
                  Cipher cipher = Cipher.getInstance(encryptAlgorithm);
                  SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), encryptKeyAlgorithm);
                  byte[] iv = decodeFromBase64(data.substring(0, 24));
                  data = data.substring(24);
                  cipher.init(2, secretKey, new GCMParameterSpec(aesKeySize, iv));
                  byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
                  data = new String(bytes, CHARSET);
                  log.info("解密数据成功，解密后数据：" + data);
              } catch (Exception e) {
                  log.error("解密数据失败", e);
                  result.put("code", "401");
                  result.put("message", "解密数据失败");
                  printResult(result);
                  return result;
              }
          }
          JSONObject eventData = JSONObject.parseObject(data);
          JSONObject returnData = new JSONObject(1);
          String dataStr = null;
          switch (eventType) {
              case "CREATE_USER":
                  // 根据 username  唯一创建下游数据，如果下游数据已存在做更新处理。并返回下游id
                  returnData.put("id", eventData.getString("username"));
                  break;
              case "CREATE_ORGANIZATION":
                  // 根据code唯一创建下游数据，如果下游数据已存在做更新处理。并返回下游id
                  returnData.put("id", eventData.getString("code"));
                  break;
              case "UPDATE_USER":
              case "UPDATE_ORGANIZATION":
                  // 更新数据时，没有修改的字段传值为空
                  returnData.put("id", eventData.getString("id"));
                  break;
              case "DELETE_ORGANIZATION":
              case "DELETE_USER":
                  // 根据下游业务逻辑进行删除操作。不需要返回字段
                  break;
              default:
                  result.put("code", "400");
                  result.put("message", "不支持的事件类型");
                  printResult(result);
                  return result;
          }
          if (dataStr == null && returnData.size() > 0)
              dataStr = returnData.toJSONString();
          if (StringUtils.isNotEmpty(encryptionKey) && dataStr != null) {
              String random = RandomStringUtils.random(24, true, true);
              log.info("开始加密数据" + dataStr);
              try {
                  Cipher cipher = Cipher.getInstance(encryptAlgorithm);
                  SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), encryptKeyAlgorithm);
                  byte[] iv = decodeFromBase64(random);
                  cipher.init(1, secretKey, new GCMParameterSpec(aesKeySize, iv));
                  byte[] bytes = dataStr.getBytes(CHARSET);
                  dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
                  dataStr = random + dataStr;
                  log.info("加密数据成功，加密后数据：" + dataStr);
              } catch (Exception e) {
                  log.error("加密数据失败", e);
                  result.put("code", "500");
                  result.put("message", "加密数据失败");
                  printResult(result);
                  return result;
              }
          }
          result.put("data", dataStr);
          printResult(result);
          return result;
      }
      private static byte[] decodeFromBase64(String data) {
          return Base64.getDecoder().decode(data);
      }
      private void printResult(JSONObject result) {
          log.info("" + result.toJSONString());
      }
  }
  ```
  

- 以下为数据签名\& AES/ECB/PKCS5Padding算法加密解密的Java语言示例
  ```
  package com.example.demo.controller;
  import com.alibaba.fastjson.JSONObject;
  import java.nio.charset.Charset;
  import java.nio.charset.StandardCharsets;
  import java.util.Base64;
  import java.util.UUID;
  import javax.crypto.Cipher;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import javax.servlet.http.HttpServletRequest;
  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.util.Assert;
  import org.springframework.web.bind.annotation.RequestBody;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.bind.annotation.ResponseBody;
  @RequestMapping
  @Controller
  public class DemoController {
      private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.DemoController.class);
      /** 签名拼接符 **/
      private static String SEPARATOR = "&";
      /** 签名密钥 **/
      private static String signKey;
      /** 加密密钥 **/
      private static String encryptionKey;
      /** 安全令牌 **/
      private static String token;
      /** 签名算法 **/
      private static String hmacSha256;
      /** 加密算法 **/
      private static String encryptAlgorithm;
      /** **/
      private static String encryptKeyAlgorithm;
      /** 字符编码 **/
      private static final Charset CHARSET = StandardCharsets.UTF_8;
      public static String getSignKey() {
          return signKey;
      }
      public static String getEncryptionKey() {
          return encryptionKey;
      }
      public static String getToken() {
          return token;
      }
      public static String getHmacSha256() {
          return hmacSha256;
      }
      public static String getEncryptAlgorithm() {
          return encryptAlgorithm;
      }
      public static String getEncryptKeyAlgorithm() {
          return encryptKeyAlgorithm;
      }
      public static void setSignKey(String signKey) {
          DemoController.signKey = signKey;
      }
      public static void setEncryptionKey(String encryptionKey) {
          DemoController.encryptionKey = encryptionKey;
      }
      public static void setToken(String token) {
          DemoController.token = token;
      }
      public static void setHmacSha256(String hmacSha256) {
          DemoController.hmacSha256 = hmacSha256;
      }
      public static void setEncryptAlgorithm(String encryptAlgorithm) {
          DemoController.encryptAlgorithm = encryptAlgorithm;
      }
      public static void setEncryptKeyAlgorithm(String encryptKeyAlgorithm) {
          DemoController.encryptKeyAlgorithm = encryptKeyAlgorithm;
      }
      @ResponseBody
      @RequestMapping({"/callback"})
      public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
          JSONObject result = new JSONObject();
          result.put("code", "200");
          result.put("message", "success");
          log.info("接收事件回调消息数据,原始报文: " + body);
          String authorization = httpRequest.getHeader("Authorization");
          if (!StringUtils.join("Bearer ", token).equals(authorization)) {
              result.put("code", "401");
              result.put("message", "不合法的请求!");
              printResult(result);
              return result;
          }
          JSONObject request = JSONObject.parseObject(body);
          String signature = request.getString("signature");
          String eventType = request.getString("eventType");
          log.info("事件类型：" + eventType);
          String data = request.getString("data");
          if (StringUtils.isNotEmpty(signKey)) {
              try {
                  log.info("开始校验签名");
                  String message = request.getString("nonce") + SEPARATOR + request.getLong("timestamp") + SEPARATOR + eventType + SEPARATOR + data;
                  Mac mac = Mac.getInstance(hmacSha256);
                  SecretKeySpec secretKey = new SecretKeySpec(signKey.getBytes(CHARSET), hmacSha256);
                  mac.init(secretKey);
                  String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(CHARSET)));
                  Assert.isTrue(newSignature.equals(signature), "签名不一致");
                  log.info("签名校验成功");
              } catch (Exception e) {
                  log.info("签名校验失败");
                  log.error("签名校验失败", e);
                  result.put("code", "401");
                  result.put("message", "签名校验失败");
                  printResult(result);
                  return result;
              }
          }
          if (StringUtils.isNotEmpty(encryptionKey)) {
              log.info("开始解密数据:  " + data);
              try {
                  Cipher cipher = Cipher.getInstance(encryptAlgorithm);
                  SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), encryptKeyAlgorithm);
                  cipher.init(2, secretKey);
                  byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
                  data = StringUtils.split(new String(bytes, CHARSET), SEPARATOR)[1];
                  log.info("解密数据成功，解密后数据: " + data);
              } catch (Exception e) {
                  log.info("解密数据失败");
                  log.error("解密数据异常", e);
                  result.put("code", "401");
                  result.put("message", "解密数据失败");
                  printResult(result);
                  return result;
              }
          }
          JSONObject eventData = JSONObject.parseObject(data);
          JSONObject returnData = new JSONObject(1);
          String dataStr = null;
          switch (eventType) {
              case "CREATE_USER":
                  returnData.put("id", eventData.getString("username"));
                  break;
              case "CREATE_ORGANIZATION":
                  returnData.put("id", eventData.getString("code"));
                  break;
              case "UPDATE_USER":
              case "UPDATE_ORGANIZATION":
                  returnData.put("id", eventData.getString("id"));
                  break;
              case "DELETE_ORGANIZATION":
              case "DELETE_USER":
                  break;
              case "CHECK_URL":
                  dataStr = UUID.randomUUID().toString().replaceAll("-", "");
                  break;
              default:
                  result.put("code", "400");
                  result.put("message", "不支持的事件类型");
                  printResult(result);
                  return result;
          }
          if (dataStr == null && returnData.size() > 0) {
              dataStr = returnData.toJSONString();
          }
          if (StringUtils.isNotEmpty(encryptionKey) && dataStr != null) {
              dataStr = RandomStringUtils.random(16, true, false) + SEPARATOR + dataStr;
              log.info("开始加密数据: " + dataStr);
              try {
                  Cipher cipher = Cipher.getInstance(encryptAlgorithm);
                  SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), encryptKeyAlgorithm);
                  cipher.init(1, secretKey);
                  byte[] bytes = dataStr.getBytes(CHARSET);
                  dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
                  log.info("加密数据成功,加密后数据: " + dataStr);
              } catch (Exception e) {
                  log.info("加密数据失败");
                  log.error("加密数据异常:", e);
                  result.put("code", "500");
                  result.put("message", "加密数据失败");
                  printResult(result);
                  return result;
              }
          }
          result.put("data", dataStr);
          printResult(result);
          return result;
      }
      private void printResult(JSONObject result) {
          log.info("返回数据为: " + result.toJSONString());
      }
  }
  ```
  
 
