前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >钉钉企业应用网关接入(保姆级教程)

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

作者头像
时间静止不是简史
发布2022-09-28 19:05:17
1.5K0
发布2022-09-28 19:05:17
举报
文章被收录于专栏:Java探索之路

背景

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

常用内网穿工具透比较

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

企业应用网关

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

是什么

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

用户痛点

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

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

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

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

钉钉企业应用网关总体架构,如下图所示

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

企业用户内网应用访问推荐以下五种解决方案

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

怎么用

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

搭建和踩坑过程

搭建过程

准备工作

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

查看企业网关应用

  1. 当与客户侧沟通成功扫描上方二维码后,客户侧可以通过下图获取到开通成功的提示信息:
在这里插入图片描述
在这里插入图片描述
  1. 在钉钉的管理后台 -> 工作台 -> 三方应用里面看到钉钉安全网关的入口,点击即可进入安全网关后台
在这里插入图片描述
在这里插入图片描述

进入企业网关首页

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

企业网关首页

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

配置连通器

  1. 前提准备 需要注意, 如果需要部署在linux服务器, 则需要满足下面条件(Windows也需保证满足如下条件)
在这里插入图片描述
在这里插入图片描述
  1. 创建连通器 选择新建连通器, 选择部署类型之后, 点击继续, 生成以下命令, 然后复制下面linux命令
在这里插入图片描述
在这里插入图片描述
  1. 在linux服务器上进行执行复制的命令后, 可以看到连通器已经启动成功
在这里插入图片描述
在这里插入图片描述
  1. 新建连通器组 将新建的连通器加入连通器组, 连通器组的作用: 批量管理连通器, 在后面应用管理时使用
在这里插入图片描述
在这里插入图片描述

配置应用管理

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

方式二: 在钉钉开放平台根据使用场景去创建应用

在这里插入图片描述
在这里插入图片描述
  1. 根据需要填写应用信息
在这里插入图片描述
在这里插入图片描述
  1. 配置成功后, 自动生成AgentId, AppKey, AppSecret, 用于接口对接时使用
在这里插入图片描述
在这里插入图片描述
  1. 发布应用(开发程序完成后使用) 应用发布后, 才能被其他用户看到(这里指在自己的工作台上看到, 而不是管理后台的工作台上看到). 体验版本发布属于灰度发布.点击查看应用发布介绍
在这里插入图片描述
在这里插入图片描述
配置应用
  1. 创建好应用之后, 在应用管理里面配置测试应用, 点击未配置的应用,进行相关配置
在这里插入图片描述
在这里插入图片描述
  1. 应用管理 - 基础配置 点击连通器选项的“+”,选择部署的连通器(或连通器组),与测试应用建立连接. 选择域名进行配置, 这里的域名值得是内网地址+端口(可以配置多条映射). 配置好保存之后即可实现内网穿透!!!
在这里插入图片描述
在这里插入图片描述
高级配置

注意:

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

因为背景介绍的原因, 需要钉钉开放平台在创建的应用中进行事件回调, 然而在我们配置好访问策略时, 钉钉回调接口时仍出现: url 地址访问异常, 不允许3xxx跳转 故需要我们管理平台-应用管理中进行高级配置

在这里插入图片描述
在这里插入图片描述
  1. 高级配置 添加允许匿名访问url: 格式为 https://生成的外网访问域名/事件回调sdk接口地址.*
在这里插入图片描述
在这里插入图片描述

2. 配置成功后, 进行测试 需要先将事件回调sdk部署在内网服务器上才能够进行测试

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

配置访问策略

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

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

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

  1. 在配置好访问策略后, 可以通过应用管理里面获取新域名, 而不是通过连通器显示的公网ip进行访问
在这里插入图片描述
在这里插入图片描述
  1. 可以看到我们每次配置的内网ip+端口都被映射成外网对应可以访问的唯一域名, 根据需要复制新域名即可
在这里插入图片描述
在这里插入图片描述
  1. 测试企业网关内网穿透效果 内网访问
在这里插入图片描述
在这里插入图片描述

公网访问

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

附: 钉钉事件回调sdk

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

  1. 钉钉开放平台加解密方法
代码语言:javascript
复制
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. Controller层接口
代码语言:javascript
复制
  /**
     * 事件回调方法
     *
     * @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;
    }

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 企业应用网关
    • 是什么
      • 怎么用
      • 搭建和踩坑过程
        • 搭建过程
          • 准备工作
          • 查看企业网关应用
          • 配置连通器
          • 配置应用管理
          • 配置访问策略
          • 获取外网访问域名(踩坑)
      • 附: 钉钉事件回调sdk
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档