• 钉钉企业应用网关接入(保姆级教程)


    背景

    在对接钉钉开放平台时, 会出现需要钉钉开放平台回调我们项目的情况. 而一般项目都被部署在公司内网.
    因此, 我们需要进行内网穿透. 常用内网穿透工具对比如下表. 可以看到钉钉是在对接钉钉开放平台时, 最优的选择…
    本文将详细介绍自己和钉钉企业应用网关对接和搭建的整体流程

    常用内网穿工具透比较

    在这里插入图片描述

    企业应用网关

    其实在之前, 钉钉还提供一种内网穿透. 但因为因安全合规、服务资源和维护成本等原因,钉钉于2022年7月21日起,不再提供内网穿透的工具服务,若需要在本地或开发测试环境调试中有内网穿透的需求,请参考文档自行搭建的frp内网穿透服务. 因此, 如果企业有这方面需求, 本着安全性, 稳定性, 简易性来说, 钉钉企业应用网关也是一个不错的选择. 而且可以联系商务搭建试用版平台. 点击进入官网地址

    是什么

    企业应用网关, 又称为钉钉零信任网关, 作用是为企业提供了内网应用在外网安全访问的能力,可以替代传统的VPN方案,基于阿里云的网络加速能力提升应用访问速度. 该产品以零信任为理念,提供持续动态的访问准入校验,最大程度上保障企业数字信息安全

    用户痛点

    企业通常会将核心应用系统放入企业内网或DMZ区,通过防火墙建立网络边界隔离.
    如果企业员工在互联网侧通过移动设备或PC设备访问企业内网应用时,通过使用2种方式:VPN拨号和端口映射.
    但是由于VPN存在设备漏洞或端口映射到外网IP或URL,黑客可以直接访问或攻击企业应用,导致企业核心数据泄露

    在这里插入图片描述

    企业原有内网应用访问方式,如下图所示

    在这里插入图片描述

    钉钉企业应用网关总体架构,如下图所示
    在这里插入图片描述
    企业用户内网应用访问推荐以下五种解决方案
    在这里插入图片描述

    怎么用

    如果习惯看官网做的话, 可以参照企业应用网关配置流程 进行搭建.

    搭建和踩坑过程

    搭建过程

    准备工作

    1. 已开通企业应用网关。如果未开通,需要使用移动端钉钉扫描下方二维码或进入此页面扫码二维码
      并安装钉钉企业应用网关应用
      在这里插入图片描述
    2. 扫描后会出现下图页面
      如果企业开通该功能, 需要该账户级别为管理员(子管理员也不行)
      在这里插入图片描述

    查看企业网关应用

    1. 当与客户侧沟通成功扫描上方二维码后,客户侧可以通过下图获取到开通成功的提示信息:
      在这里插入图片描述

    2. 在钉钉的管理后台 -> 工作台 -> 三方应用里面看到钉钉安全网关的入口,点击即可进入安全网关后台
      在这里插入图片描述
      进入企业网关首页
      在这里插入图片描述
      企业网关首页
      在这里插入图片描述

    配置连通器

    1. 前提准备
      需要注意, 如果需要部署在linux服务器, 则需要满足下面条件(Windows也需保证满足如下条件)
      在这里插入图片描述

    2. 创建连通器
      选择新建连通器, 选择部署类型之后, 点击继续, 生成以下命令, 然后复制下面linux命令
      在这里插入图片描述

    3. 在linux服务器上进行执行复制的命令后, 可以看到连通器已经启动成功
      在这里插入图片描述

    4. 新建连通器组
      将新建的连通器加入连通器组, 连通器组的作用: 批量管理连通器, 在后面应用管理时使用
      在这里插入图片描述

    补充——连通器自启动配置

    2022.12.12补充, 公司在进行网络调整时, 由于网络断开过导致连通器断开, 为了图方便进行了服务器重启. 但是由于重启后连通器也未进行重启. 所以带来了一系列的问题. 需要配置重启后自启动. 当前服务器为ubuntu, 因此需要在/etc/rc.local 下面配置自启动项.

    # 进入rc.local
    vim  /etc/rc.local
    # 配置启动名称 
    ## cd 后面跟的是连通器脚本所在目录, 就是执行第2步创建连通器是所在创建的目录
    ## ;(分号)后面执行的命令就是图2圈出来的参数!!!
    cd /home/dingding-getway/connector ;./start.sh -a endpoint.ztna-dingtalk.com:8021 -k 6cc96011442d42149bdfbf95a1c61343 -s 42e4d95397dd4a75bf8b51e1ac655026df1786927f77f4a788e951b9d79fc55b &
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述

    配置应用管理

    准备前提
    1. 新建应用
      方式一: 在管理后台-工作台新建第三方应用
      在这里插入图片描述

      方式二: 在钉钉开放平台根据使用场景去创建应用
      在这里插入图片描述

    2. 根据需要填写应用信息
      在这里插入图片描述

    3. 配置成功后, 自动生成AgentId, AppKey, AppSecret, 用于接口对接时使用
      在这里插入图片描述

    4. 发布应用(开发程序完成后使用)
      应用发布后, 才能被其他用户看到(这里指在自己的工作台上看到, 而不是管理后台的工作台上看到). 体验版本发布属于灰度发布.点击查看应用发布介绍
      在这里插入图片描述

    配置应用
    1. 创建好应用之后, 在应用管理里面配置测试应用, 点击未配置的应用,进行相关配置
      在这里插入图片描述

    2. 应用管理 - 基础配置
      点击连通器选项的“+”,选择部署的连通器(或连通器组),与测试应用建立连接.
      选择域名进行配置, 这里的域名值得是内网地址+端口(可以配置多条映射). 配置好保存之后即可实现内网穿透!!!
      在这里插入图片描述

    3. 需要说明的是, 这里的主域名配置就是你在钉钉开放平台->具体应用->开发管理的应用首页地址. 如果后面配置规则之后访问出现unexpected EOF, 则说明配置的url在连通器上面访问不通, 一般情况就是在开发配置中, 配置的是https但实际上内网走的是http
      在这里插入图片描述 在这里插入图片描述
      例如我上图配置的内网域名是https, 但是实际上curl https://ip:port 则会出错. 说明该服务在内网本身是通过http访问的, 但是在网关域名中我误写成https导致网关穿透失败(因为该域名本身在内网都访问不通!!!)
      在这里插入图片描述

      因此在钉钉开放平台->具体应用->开发管理的应用首页地址之后访问不通时, 一定要验证该url在钉钉连通器所在服务器是否可以访问!

    高级配置

    注意:

    • 下面介绍下高级配置. 如无特殊需求可以不进行高级配置(直接进行下一步).
    • 这里配置需的前提是 要我们在下一步访问策略配置并获取外网访问域名后 再进行配置

    因为背景介绍的原因, 需要钉钉开放平台在创建的应用中进行事件回调(在公司组织架构进行变动时调用该接口),
    然而在我们配置好访问策略时, 钉钉回调接口时仍出现: url 地址访问异常, 不允许3xxx跳转
    故需要我们管理平台-应用管理中进行高级配置. 没有配置前, 填入url后报错, 内容如下图所示

    在这里插入图片描述

    1. 高级配置
      添加允许匿名访问url: 格式为 https://生成的外网访问域名/事件回调sdk接口地址.*
      在这里插入图片描述
    2. 配置成功后, 进行测试
      需要先将事件回调sdk部署在内网服务器上才能够进行测试
      在这里插入图片描述

    配置访问策略

    策略管理由企业管理员操作,包括注册策略、修改策略、删除策略、停用策略、启用策略、优先级排序等

    1. 然后点击“创建策略”按钮,进入创建策略界面
      在这里插入图片描述
    2. 在策略页面,填写策略信息,配置项如下表
      在这里插入图片描述
      在这里插入图片描述
    3. 点击完成,创建策略
      在这里插入图片描述

    获取外网访问域名(踩坑)

    1. 在配置好访问策略后, 可以通过应用管理里面获取新域名, 而不是通过连通器显示的公网ip进行访问
      在这里插入图片描述

    2. 可以看到我们每次配置的内网ip+端口都被映射成外网对应可以访问的唯一域名, 根据需要复制新域名即可
      在这里插入图片描述

    3. 测试企业网关内网穿透效果
      内网访问
      在这里插入图片描述
      公网访问
      在这里插入图片描述

    附: 钉钉事件回调sdk

    事件回调介绍地址: https://open.dingtalk.com/document/org/configure-event-subcription

    1. 钉钉开放平台加解密方法

      
      import java.io.ByteArrayOutputStream;
      import java.nio.charset.Charset;
      import java.security.MessageDigest;
      import java.security.Permission;
      import java.security.PermissionCollection;
      import java.util.Arrays;
      import java.util.HashMap;
      import java.util.Map;
      import java.util.Random;
      import java.security.Security;
      import java.lang.reflect.Field;
      
      import javax.crypto.Cipher;
      import javax.crypto.spec.IvParameterSpec;
      import javax.crypto.spec.SecretKeySpec;
      
      import com.alibaba.fastjson.JSON;
      
      import org.apache.commons.codec.binary.Base64;
      
      /**
       * 钉钉开放平台加解密方法
       * 在ORACLE官方网站下载JCE无限制权限策略文件
       * JDK6的下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
       * JDK7的下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
       * JDK8的下载地址 https://www.oracle.com/java/technologies/javase-jce8-downloads.html
       * @Author caoHaiYang
       * @Date 2022/8/18 10:15
       */
      public class DingCallbackCrypto {
      
          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         企业自建应用-事件订阅, 使用appKey
           *                       企业自建应用-注册回调地址, 使用corpId
           *                       第三方企业应用, 使用suiteKey
           *
           * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
           */
          public DingCallbackCrypto(String token, String encodingAesKey, String corpId) throws DingTalkEncryptException {
              if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL);
              }
              this.token = token;
              this.corpId = corpId;
              aesKey = Base64.decodeBase64(encodingAesKey + "=");
          }
      
          public Map<String, String> getEncryptedMap(String plaintext) throws DingTalkEncryptException {
              return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
          }
      
          /**
           * 将和钉钉开放平台同步的消息体加密,返回加密Map
           *
           * @param plaintext 传递的消息体明文
           * @param timeStamp 时间戳
           * @param nonce     随机字符串
           * @return
           * @throws DingTalkEncryptException
           */
          public Map<String, String> getEncryptedMap(String plaintext, Long timeStamp, String nonce)
                  throws DingTalkEncryptException {
              if (null == plaintext) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
              }
              if (null == timeStamp) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
              }
              if (null == nonce) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL);
              }
              // 加密
              String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
              String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
              Map<String, String> resultMap = new HashMap<String, String>();
              resultMap.put("msg_signature", signature);
              resultMap.put("encrypt", encrypt);
              resultMap.put("timeStamp", String.valueOf(timeStamp));
              resultMap.put("nonce", nonce);
              return resultMap;
          }
      
          /**
           * 密文解密
           *
           * @param msgSignature 签名串
           * @param timeStamp    时间戳
           * @param nonce        随机串
           * @param encryptMsg   密文
           * @return 解密后的原文
           * @throws DingTalkEncryptException
           */
          public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
                  throws DingTalkEncryptException {
              //校验签名
              String signature = getSignature(token, timeStamp, nonce, encryptMsg);
              if (!signature.equals(msgSignature)) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
              }
              // 解密
              String result = decrypt(encryptMsg);
              return result;
          }
      
          /*
           * 对明文加密.
           * @param text 需要加密的明文
           * @return 加密后base64编码的字符串
           */
          private String encrypt(String random, String plaintext) throws DingTalkEncryptException {
              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 DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
              }
          }
      
          /*
           * 对密文进行解密.
           * @param text 需要解密的密文
           * @return 解密得到的明文
           */
          private String decrypt(String text) throws DingTalkEncryptException {
              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 DingTalkEncryptException(DingTalkEncryptException.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 DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
              }
      
              // corpid不相同的情况
              if (!fromCorpid.equals(corpId)) {
                  throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
              }
              return plainText;
          }
      
          /**
           * 数字签名
           *
           * @param token     isv token
           * @param timestamp 时间戳
           * @param nonce     随机串
           * @param encrypt   加密文本
           * @return
           * @throws DingTalkEncryptException
           */
          public String getSignature(String token, String timestamp, String nonce, String encrypt)
                  throws DingTalkEncryptException {
              try {
                  String[] array = new String[] {token, timestamp, nonce, encrypt};
                  Arrays.sort(array);
                  //System.out.println(JSON.toJSONString(array));
                  StringBuffer sb = new StringBuffer();
                  for (int i = 0; i < 4; i++) {
                      sb.append(array[i]);
                  }
                  String str = sb.toString();
                  System.out.println(str);
                  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 DingTalkEncryptException(DingTalkEncryptException.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 DingTalkEncryptException 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 DingTalkEncryptException(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));
          }
      
      }
      
      
      • 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
      • 212
      • 213
      • 214
      • 215
      • 216
      • 217
      • 218
      • 219
      • 220
      • 221
      • 222
      • 223
      • 224
      • 225
      • 226
      • 227
      • 228
      • 229
      • 230
      • 231
      • 232
      • 233
      • 234
      • 235
      • 236
      • 237
      • 238
      • 239
      • 240
      • 241
      • 242
      • 243
      • 244
      • 245
      • 246
      • 247
      • 248
      • 249
      • 250
      • 251
      • 252
      • 253
      • 254
      • 255
      • 256
      • 257
      • 258
      • 259
      • 260
      • 261
      • 262
      • 263
      • 264
      • 265
      • 266
      • 267
      • 268
      • 269
      • 270
      • 271
      • 272
      • 273
      • 274
      • 275
      • 276
      • 277
      • 278
      • 279
      • 280
      • 281
      • 282
      • 283
      • 284
      • 285
      • 286
      • 287
      • 288
      • 289
      • 290
      • 291
      • 292
      • 293
      • 294
      • 295
      • 296
      • 297
      • 298
      • 299
      • 300
      • 301
      • 302
      • 303
      • 304
      • 305
      • 306
      • 307
      • 308
      • 309
      • 310
      • 311
      • 312
      • 313
      • 314
      • 315
      • 316
      • 317
      • 318
      • 319
      • 320
      • 321
      • 322
      • 323
      • 324
      • 325
      • 326
      • 327
      • 328
      • 329
      • 330
      • 331
      • 332
      • 333
      • 334
      • 335
      • 336
      • 337
      • 338
      • 339
      • 340
      • 341
      • 342
      • 343
      • 344
      • 345
      • 346
      • 347
      • 348
      • 349
      • 350
      • 351
      • 352
      • 353
      • 354
      • 355
      • 356
      • 357
      • 358
      • 359
      • 360
      • 361
      • 362
      • 363
      • 364
      • 365
      • 366
      • 367
      • 368
      • 369
      • 370
      • 371
      • 372
      • 373
      • 374
      • 375
      • 376
      • 377
      • 378
      • 379
      • 380
      • 381
      • 382
      • 383
      • 384
      • 385
      • 386
      • 387
      • 388
      • 389
      • 390
      • 391
      • 392
      • 393
      • 394
      • 395
      • 396
      • 397
      • 398
      • 399
      • 400
      • 401
      • 402
    2. Controller层接口

        /**
           * 事件回调方法
           *
           * @param msg_signature
           * @param timeStamp
           * @param nonce
           * @param json
           * @return
           */
          @RequestMapping("/callback")
          public Map<String, String> callBack(
                  @RequestParam(value = "msg_signature", required = false) String msg_signature,
                  @RequestParam(value = "timestamp", required = false) String timeStamp,
                  @RequestParam(value = "nonce", required = false) String nonce,
                  @RequestBody(required = false) JSONObject json) {
              try {
                  // 1. 从http请求中获取加解密参数
                  // 2. 使用加解密类型
                  // Constant.OWNER_KEY 说明:
                  // 1、开发者后台配置的订阅事件为应用级事件推送,此时OWNER_KEY为应用的APP_KEY。
                  // 2、调用订阅事件接口订阅的事件为企业级事件推送,
                  //      此时OWNER_KEY为:企业的appkey(企业内部应用)或SUITE_KEY(三方应用)
                  DingCallbackCrypto callbackCrypto = new DingCallbackCrypto(dingTalkConfig.getToken(), dingTalkConfig.getAesKey(), dingTalkConfig.getAppkey());
                  String encryptMsg = json.getString("encrypt");
                  String decryptMsg = callbackCrypto.getDecryptMsg(msg_signature, timeStamp, nonce, encryptMsg);
                  // 3. 反序列化回调事件json数据
                  JSONObject eventJson = JSON.parseObject(decryptMsg);
                  log.info("反序列化回调事件json数据:" + eventJson);
                  String eventType = eventJson.getString("EventType");
                  // 4. 根据EventType分类处理
                  if ("check_url".equals(eventType)) {
                      // 测试回调url的正确性
                      log.info("测试回调url的正确性");
                  } else if ("user_add_org".equals(eventType)) {
                      // 处理通讯录用户增加事件
                      log.info("发生了:" + eventType + "事件");
                  } else {
                      // 添加其他已注册的
                      log.info("发生了:" + eventType + "事件");
                  }
      
                  // 5. 返回success的加密数据
                  Map<String, String> successMap = callbackCrypto.getEncryptedMap("success");
                  return successMap;
      
              } catch (DingCallbackCrypto.DingTalkEncryptException e) {
                  e.printStackTrace();
              }
              return null;
          }
      
      • 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
    3. 钉钉提供的事件回调列表
      其他类型请点击此蓝色链接获取
      在这里插入图片描述
      在这里插入图片描述

    补充: 前端联调时, 需要为前端配置步骤

    在复用钉钉登录功能之后, 前端进行本地调试时, 会进行网关内网穿透, 以便外网可测试访问.
    当然也可以直接构造内网的二维码登录url, 但这里我们着重讨论第一种情况. 探究其解决方法

    1. 钉钉企业应用网关平台, 配置对应应用的内网ip+端口到外网的映射
      在这里插入图片描述

    2. 找到并复制映射好的外网映射
      在这里插入图片描述

    3. 在钉钉开放平台的企业应用开发中, 选择自己的应用. 配置登录回调
      这里直接配置内网地址, 或者上一步映射好的外网地址均可
      在这里插入图片描述
      如果本步骤没有进行配置, 前端在访问该页面则会出现下面问题
      在这里插入图片描述


    2023-01-03

    补充: 如何配置网关连通器开机自启动

    ubuntu

    整体思路是按照这个大佬的博客走 Ubuntu设置自启动软件 本人使用的服务器是ubuntu 18.04

    1. 第一步:检查系统目录/lib/systemd/system/rc-local.service,如果没有自己新建,文件内容为(如果文件存在本身是没有[Install]项的,需要自己添加进去)

      #  SPDX-License-Identifier: LGPL-2.1+
      #
      #  This file is part of systemd.
      #
      #  systemd is free software; you can redistribute it and/or modify it
      #  under the terms of the GNU Lesser General Public License as published by
      #  the Free Software Foundation; either version 2.1 of the License, or
      #  (at your option) any later version.
       
      # This unit gets pulled automatically into multi-user.target by
      # systemd-rc-local-generator if /etc/rc.local is executable.
      [Unit]
      Description=/etc/rc.local Compatibility
      Documentation=man:systemd-rc-local-generator(8)
      ConditionFileIsExecutable=/etc/rc.local
      After=network.target
       
      [Service]
      Type=forking
      ExecStart=/etc/rc.local start
      TimeoutSec=0
      RemainAfterExit=yes
      GuessMainPID=no
       
      [Install]
      WantedBy=multi-user.target
      Alias=rc-local.service
      
      • 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

      在这里插入图片描述

    2. etc目录下的文件也需要进行如上修改,检查/etc/systemd/system/rc-local.service,如果没有该文件则新增该文件

      	#  SPDX-License-Identifier: LGPL-2.1+
      	#
      	#  This file is part of systemd.
      	#
      	#  systemd is free software; you can redistribute it and/or modify it
      	#  under the terms of the GNU Lesser General Public License as published by
      	#  the Free Software Foundation; either version 2.1 of the License, or
      	#  (at your option) any later version.
      	 
      	# This unit gets pulled automatically into multi-user.target by
      	# systemd-rc-local-generator if /etc/rc.local is executable.
      	[Unit]
      	Description=/etc/rc.local Compatibility
      	Documentation=man:systemd-rc-local-generator(8)
      	ConditionFileIsExecutable=/etc/rc.local
      	After=network.target
      	 
      	[Service]
      	Type=forking
      	ExecStart=/etc/rc.local start
      	TimeoutSec=0
      	RemainAfterExit=yes
      	GuessMainPID=no
      	 
      	[Install]
      	WantedBy=multi-user.target
      	Alias=rc-local.service
      	```
      
      
      • 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
    3. 创建/etc/rc.local脚本文件,并写入想要运行的脚本命令

      vim /etc/rc.local
      
      • 1

      在这里插入图片描述

    4. 给rc.local执行的权限

      sudo chmod +x /etc/rc.local
      
      • 1
    5. 启用服务

      sudo systemctl enable rc-local
      sudo systemctl start rc-local.service
      sudo systemctl status rc-local.service
      
      • 1
      • 2
      • 3

      在这里插入图片描述

    6. 重启电脑看效果
      通过重启后查看脚本启动的应用是否能够正常运行
      在这里插入图片描述

    centos操作系统

    参考 Linux设置开机自启动的三种方法

    1. 根据系统文件配置, 找到对应启动脚本, 系统文件所在位置 /lib/systemd/system/rc-local.service
      在这里插入图片描述

    2. 编辑启动脚本 vim /etc/rc.d/rc.local
      在这里插入图片描述

    3. 授予脚本可执行权限 chmod +x /etc/rc.d/rc.local

    4. 重启后测试软件是否启动
      在这里插入图片描述

  • 相关阅读:
    MybatisPlusGenerator代码生成工具配置教程
    Linux·软中断&tasklet
    算法竞赛进阶指南 基本数据结构 0x14哈希表
    【LeetCode】31. 下一个排列
    【C语言】Debian安装并编译内核源码
    【web网页设计期末课程大作业】基于HTML+CSS+JavaScript仿山东传媒职业学院网站
    mqtt.js在接收到数据后,如何将数据实时更新到vue界面上的data中
    VS2010 C语言内嵌汇编语言程序
    JVS开发套件产品定位
    大厂FPGA的面试题
  • 原文地址:https://blog.csdn.net/qq_43371556/article/details/126600171