事件订阅对接

校园大脑会向应用推送订阅的事件,例如 机构变更用户变更教师变更学生变更角色变更等。通过订阅这些事件,可以更好地与新立方集成。你只需告诉新立方当某个事件发生时,新立方需要推送消息到哪个URL,新立方会以HTTP POST请求的方式将事件内容以JSON格式推送给你。

事件订阅流程

  1. 1三方应用开发回调接口
  2. 2在应用详情事件与回调配置回调地址和加密信息
  3. 3新立方向三方接口推送check_url事件
  4. 4 三方服务返回 包含”success” 的加密字符串

订阅申请

  1. 1进入开发者后台,找到已创建的应用,进入应用详情。
  2. 2单击事件与回调,然后单击编辑,配置用于接收请求的HTTP地址和加密信息。
  3. 3编辑完成,点击保存按钮,新立方会发送一个接口推送check_url事件,application/json 格式的 POST 请求,以验证你配置的回调地址合法性。
  4. 4 你需要通过密文进行解密,监听eventType ,并在1500ms内返回包含success的加密字符串 (JSON格式),否则判定回调地址不合法
  5. 5成功配置请求地址后,在事件订阅列表区域,申请要订阅的事件。

接收并响应事件

当事件发生时,新立方会主动向配置的HTTP地址发送POST请求,推送对应的事件信息。例如订阅机构变更事件后,当机构发生变更时,会向注册的HTTP地址推送事件信息,其格式如下。
其中:

  • msg_signature。
  • timeStamp为时间戳。
  • nonce为随机字符串。
  • encrypt为事件加密字符串。
    http://你注册的HTTP地址
    {
    "msg_signature":"111108bb8e6dbce3c9671d6fdb69d1506xxxx",
    "timeStamp":"1783610513",
    "nonce":"123456",
    "encrypt":"1ojQf0NSvw2WPvW7LijxS8UvISr8pdDP+rXpPbcLGOmIxxxx"
    }
    回调实例代码如下
    @PostMapping("callBack")
      public CallBackMessageVO callBack(@RequestBody CallBackMessageVO backVO){
          CallBackMessageVO successVo = new CallBackMessageVO();
          try{
              XlfCallbackCrypto callbackCrypto = new XlfCallbackCrypto(Constant.AES_TOKEN, Constant.AES_KEY, Constant.CLIENT_ID);
              String encryptMsg = backVO.getEncrypt();
              String decryptMsg = callbackCrypto.getDecryptMsg(backVO.getMsg_signature(), backVO.getTimeStamp(), backVO.getNonce(), encryptMsg);
              // 3. 反序列化回调事件json数据
              JSONObject eventJson = JSON.parseObject(decryptMsg);
              log.info("获取消息="+decryptMsg);
              String eventType = eventJson.getString("eventType");
              switch (eventType){
                  case "check_url":
                      log.info("测试回调url的正确性");
                      break;
                  case "xxjbsjlb_c":
                      log.info("增加了机构");
                      break;
                  case "xxjbsjlb_u":
                      log.info("修改或删除了机构");
                      break;
                  default:
                      log.info("其他操作={}",eventType);
              }
              // 5. 返回success的加密数据
              successVo = callbackCrypto.getEncryptedMap("success");
          }catch (Exception e ){
              e.printStackTrace();
          }
          return successVo;
      }

    消息加解密

    为了保证数据传输的安全,新立方在推送订阅事件时,会使用密钥对消息进行加密,三方返回回调成功的消息同样需要进行加密传输
    加密工具类代码如下
public class XlfCallbackCrypto {

    private static final Charset CHARSET = Charset.forName("utf-8");
    private static final Base64 base64 = new Base64();
    private byte[] aesKey;
    private String token;
    private String corpId;
    /**
     * ask getPaddingBytes key固定长度
     **/
    private static final Integer AES_ENCODE_KEY_LENGTH = 43;
    /**
     * 加密随机字符串字节长度
     **/
    private static final Integer RANDOM_LENGTH = 16;

    /**
     * 构造函数
     *
     * @param token          新立方开放平台上,开发者设置的token
     * @param encodingAesKey 新立方开放台上,开发者设置的EncodingAESKey
     * @param corpId         企业自建应用-事件订阅, 使用clientId
     *
     * @throws XlfEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public XlfCallbackCrypto(String token, String encodingAesKey, String corpId) throws XlfEncryptException {
        if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
            throw new XlfEncryptException(XlfEncryptException.AES_KEY_ILLEGAL);
        }
        this.token = token;
        this.corpId = corpId;
        aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    public CallBackMessageVO getEncryptedMap(String plaintext) throws XlfEncryptException {
        return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
    }

    /**
     * 将和新立方开放平台同步的消息体加密,返回加密Map
     *
     * @param plaintext 传递的消息体明文
     * @param timeStamp 时间戳
     * @param nonce     随机字符串
     * @return
     * @throws XlfEncryptException
     */
    public CallBackMessageVO getEncryptedMap(String plaintext, Long timeStamp, String nonce)
            throws XlfEncryptException {
        if (null == plaintext) {
            throw new XlfEncryptException(XlfEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
        }
        if (null == timeStamp) {
            throw new XlfEncryptException(XlfEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
        }
        if (null == nonce) {
            throw new XlfEncryptException(XlfEncryptException.ENCRYPTION_NONCE_ILLEGAL);
        }
        // 加密
        String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
        String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
        CallBackMessageVO vo = new CallBackMessageVO();
        vo.setMsg_signature(signature);
        vo.setTimeStamp(String.valueOf(timeStamp));
        vo.setNonce(nonce);
        vo.setEncrypt(encrypt);
        return vo;
    }

    /**
     * 密文解密
     *
     * @param msg_signature 签名串
     * @param timeStamp    时间戳
     * @param nonce        随机串
     * @param encryptMsg   密文
     * @return 解密后的原文
     * @throws XlfEncryptException
     */
    public String getDecryptMsg(String msg_signature, String timeStamp, String nonce, String encryptMsg)
            throws XlfEncryptException {
        //校验签名
        String signature = getSignature(token, timeStamp, nonce, encryptMsg);
        if (!signature.equals(msg_signature)) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_SIGNATURE_ERROR);
        }
        // 解密
        String result = decrypt(encryptMsg);
        return result;
    }

    /*
     * 对明文加密.
     * @param text 需要加密的明文
     * @return 加密后base64编码的字符串
     */
    private String encrypt(String random, String plaintext) throws XlfEncryptException {
        try {
            byte[] randomBytes = random.getBytes(CHARSET);
            byte[] plainTextBytes = plaintext.getBytes(CHARSET);
            byte[] lengthByte = Utils.int2Bytes(plainTextBytes.length);
            byte[] corpidBytes = corpId.getBytes(CHARSET);
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            byteStream.write(randomBytes);
            byteStream.write(lengthByte);
            byteStream.write(plainTextBytes);
            byteStream.write(corpidBytes);
            byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
            byteStream.write(padBytes);
            byte[] unencrypted = byteStream.toByteArray();
            byteStream.close();
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            byte[] encrypted = cipher.doFinal(unencrypted);
            String result = base64.encodeToString(encrypted);
            return result;
        } catch (Exception e) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
        }
    }

    /*
     * 对密文进行解密.
     * @param text 需要解密的密文
     * @return 解密得到的明文
     */
    private String decrypt(String text) throws XlfEncryptException {
        byte[] originalArr;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(text);
            // 解密
            originalArr = cipher.doFinal(encrypted);
        } catch (Exception e) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
        }

        String plainText;
        String fromCorpid;
        try {
            // 去除补位字符
            byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
            // 分离16位随机字符串,网络字节序和corpId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
            int plainTextLegth = Utils.bytes2int(networkOrder);
            plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
            fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
        } catch (Exception e) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
        }

        // corpid不相同的情况
        if (!fromCorpid.equals(corpId)) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
        }
        return plainText;
    }

    /**
     * 数字签名
     *
     * @param token     isv token
     * @param timestamp 时间戳
     * @param nonce     随机串
     * @param encrypt   加密文本
     * @return
     * @throws XlfEncryptException
     */
    public String getSignature(String token, String timestamp, String nonce, String encrypt)
            throws XlfEncryptException {
        try {
            String[] array = new String[] {token, timestamp, nonce, encrypt};
            Arrays.sort(array);
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            throw new XlfEncryptException(XlfEncryptException.COMPUTE_SIGNATURE_ERROR);
        }
    }

    public static class Utils {
        public Utils() {
        }

        public static String getRandomStr(int count) {
            String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            Random random = new Random();
            StringBuffer sb = new StringBuffer();

            for (int i = 0; i < count; ++i) {
                int number = random.nextInt(base.length());
                sb.append(base.charAt(number));
            }

            return sb.toString();
        }

        public static byte[] int2Bytes(int count) {
            byte[] byteArr = new byte[] {(byte)(count >> 24 & 255), (byte)(count >> 16 & 255), (byte)(count >> 8 & 255),
                    (byte)(count & 255)};
            return byteArr;
        }

        public static int bytes2int(byte[] byteArr) {
            int count = 0;

            for (int i = 0; i < 4; ++i) {
                count <<= 8;
                count |= byteArr[i] & 255;
            }

            return count;
        }
    }

    public static class PKCS7Padding {
        private static final Charset CHARSET = Charset.forName("utf-8");
        private static final int BLOCK_SIZE = 32;

        public PKCS7Padding() {
        }

        public static byte[] getPaddingBytes(int count) {
            int amountToPad = 32 - count % 32;
            if (amountToPad == 0) {
                amountToPad = 32;
            }

            char padChr = chr(amountToPad);
            String tmp = new String();

            for (int index = 0; index < amountToPad; ++index) {
                tmp = tmp + padChr;
            }

            return tmp.getBytes(CHARSET);
        }

        public static byte[] removePaddingBytes(byte[] decrypted) {
            int pad = decrypted[decrypted.length - 1];
            if (pad < 1 || pad > 32) {
                pad = 0;
            }

            return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
        }

        private static char chr(int a) {
            byte target = (byte)(a & 255);
            return (char)target;
        }
    }

    public static class XlfEncryptException extends Exception {
        public static final int SUCCESS = 0;
        public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
        public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
        public static final int ENCRYPTION_NONCE_ILLEGAL = 900003;
        public static final int AES_KEY_ILLEGAL = 900004;
        public static final int SIGNATURE_NOT_MATCH = 900005;
        public static final int COMPUTE_SIGNATURE_ERROR = 900006;
        public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
        public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
        public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
        public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
        private static Map<Integer, String> msgMap = new HashMap();
        private Integer code;

        static {
            msgMap.put(0, "成功");
            msgMap.put(900001, "加密明文文本非法");
            msgMap.put(900002, "加密时间戳参数非法");
            msgMap.put(900003, "加密随机字符串参数非法");
            msgMap.put(900005, "签名不匹配");
            msgMap.put(900006, "签名计算失败");
            msgMap.put(900004, "不合法的aes key");
            msgMap.put(900007, "计算加密文字错误");
            msgMap.put(900008, "计算解密文字错误");
            msgMap.put(900009, "计算解密文字长度不匹配");
            msgMap.put(900010, "计算解密文字corpid不匹配");
        }

        public Integer getCode() {
            return this.code;
        }

        public XlfEncryptException(Integer exceptionCode) {
            super((String)msgMap.get(exceptionCode));
            this.code = exceptionCode;
        }
    }
    static {
        try {
            Security.setProperty("crypto.policy", "limited");
            RemoveCryptographyRestrictions();
        } catch (Exception var1) {
        }

    }
    private static void RemoveCryptographyRestrictions() throws Exception {
        Class<?> jceSecurity = getClazz("javax.crypto.JceSecurity");
        Class<?> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions");
        Class<?> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission");
        if (jceSecurity != null) {
            setFinalStaticValue(jceSecurity, "isRestricted", false);
            PermissionCollection defaultPolicy = (PermissionCollection)getFieldValue(jceSecurity, "defaultPolicy", (Object)null, PermissionCollection.class);
            if (cryptoPermissions != null) {
                Map<?, ?> map = (Map)getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class);
                map.clear();
            }

            if (cryptoAllPermission != null) {
                Permission permission = (Permission)getFieldValue(cryptoAllPermission, "INSTANCE", (Object)null, Permission.class);
                defaultPolicy.add(permission);
            }
        }

    }
    private static Class<?> getClazz(String className) {
        Class clazz = null;

        try {
            clazz = Class.forName(className);
        } catch (Exception var3) {
        }

        return clazz;
    }
    private static void setFinalStaticValue(Class<?> srcClazz, String fieldName, Object newValue) throws Exception {
        Field field = srcClazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & -17);
        field.set((Object)null, newValue);
    }
    private static <T> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception {
        Field field = srcClazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return dstClazz.cast(field.get(owner));
    }

}
作者:杭州天音  创建时间:2022-12-15 10:49
最后编辑:杭州天音  更新时间:2025-08-22 15:44