更新时间:2024-01-18 GMT+08:00
分享

签名验签说明

当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);
        private static final String SEPARATOR = "&";
    
        //region 以下三个参数均由第三方随机生成的32位字符串(英文字母大小写和数字),正式环境不要使用以下示例
        /**
         * 安全令牌
         */
        private static final String TOKEN = "4JV**********NCE";
    
        /**
         * 签名密钥
         */
        private static final String SIGN_KEY = "wGt**********Rrs";
    
        /**
         * 加密密钥
         */
        private static final String ENCRYPTION_KEY = "ZJI**********FQo";
        //endregion
    
        /**
         * 加密算法
         */
        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 int AES_KEY_SIZE = 128;
        private static final Charset CHARSET = StandardCharsets.UTF_8;
    
        @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(SIGN_KEY))
                try {
                    log.info("开始校验签名");
                    String message = request.getString("nonce") + "&" + request.getLong("timestamp") + "&" + eventType + "&" + data;
                    Mac mac = Mac.getInstance(HMAC_SHA256);
                    SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(CHARSET), HMAC_SHA256);
                    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(ENCRYPTION_KEY)) {
                log.info("开始解密数据" + data);
                try {
                    Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                    SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                    byte[] iv = decodeFromBase64(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));
                    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(ENCRYPTION_KEY) && dataStr != null) {
                String random = RandomStringUtils.random(24, true, true);
                log.info("开始加密数据" + dataStr);
                try {
                    Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                    SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                    byte[] iv = decodeFromBase64(random);
                    cipher.init(1, secretKey, new GCMParameterSpec(AES_KEY_SIZE, 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 final String SEPARATOR = "&";
    
      /** 签名密钥 **/
      private static final String SIGN_KEY = "wGt**********Rrs";
    
      /** 加密密钥 **/
      private static final String ENCRYPTION_KEY = "ZJI**********FQo";
    
      /** 安全令牌 **/
      private static final String TOKEN = "4JV**********NCE";
    
      /** 签名算法 **/
      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) {
        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(SIGN_KEY)) {
          try {
            log.info("开始校验签名");
            String message = request.getString("nonce") + SEPARATOR + request.getLong("timestamp") + SEPARATOR + eventType + SEPARATOR + data;
            Mac mac = Mac.getInstance(HMAC_SHA256);
            SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(CHARSET), HMAC_SHA256);
            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(ENCRYPTION_KEY)) {
          log.info("开始解密数据:  " + data);
          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(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(ENCRYPTION_KEY) && dataStr != null) {
          dataStr = RandomStringUtils.random(16, true, false) + SEPARATOR + dataStr;
          log.info("开始加密数据: " + 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);
            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());
      }
    }

相关文档