概述
背景
在对接钉钉开放平台时, 会出现需要钉钉开放平台回调我们项目的情况. 而一般项目都被部署在公司内网.
因此, 我们需要进行内网穿透. 常用内网穿透工具对比如下表. 可以看到钉钉是在对接钉钉开放平台时, 最优的选择…
本文将详细介绍自己和钉钉企业应用网关对接和搭建的整体流程
常用内网穿工具透比较
企业应用网关
其实在之前, 钉钉还提供一种内网穿透. 但因为因安全合规、服务资源和维护成本等原因,钉钉于2022年7月21日起,不再提供内网穿透的工具服务,若需要在本地或开发测试环境调试中有内网穿透的需求,请参考文档自行搭建的frp内网穿透服务. 因此, 如果企业有这方面需求, 本着安全性, 稳定性, 简易性来说, 钉钉企业应用网关也是一个不错的选择. 而且可以联系商务搭建试用版平台. 点击进入官网地址
是什么
企业应用网关, 又称为钉钉零信任网关, 作用是为企业提供了内网应用在外网安全访问的能力,可以替代传统的VPN方案,基于阿里云的网络加速能力提升应用访问速度. 该产品以零信任为理念,提供持续动态的访问准入校验,最大程度上保障企业数字信息安全
用户痛点
企业通常会将核心应用系统放入企业内网或DMZ区,通过防火墙建立网络边界隔离.
如果企业员工在互联网侧通过移动设备或PC设备访问企业内网应用时,通过使用2种方式:VPN拨号和端口映射.
但是由于VPN存在设备漏洞或端口映射到外网IP或URL,黑客可以直接访问或攻击企业应用,导致企业核心数据泄露
企业原有内网应用访问方式,如下图所示
钉钉企业应用网关总体架构,如下图所示
企业用户内网应用访问推荐以下五种解决方案
怎么用
如果习惯看官网做的话, 可以参照企业应用网关配置流程 进行搭建.
搭建和踩坑过程
搭建过程
准备工作
- 已开通企业应用网关。如果未开通,需要使用移动端钉钉扫描下方二维码或进入此页面扫码二维码,
并安装钉钉企业应用网关应用
- 扫描后会出现下图页面
如果企业开通该功能, 需要该账户级别为管理员(子管理员也不行)
查看企业网关应用
-
当与客户侧沟通成功扫描上方二维码后,客户侧可以通过下图获取到开通成功的提示信息:
-
在钉钉的管理后台 -> 工作台 -> 三方应用里面看到钉钉安全网关的入口,点击即可进入安全网关后台
进入企业网关首页
企业网关首页
配置连通器
-
前提准备
需要注意, 如果需要部署在linux服务器, 则需要满足下面条件(Windows也需保证满足如下条件)
-
创建连通器
选择新建连通器, 选择部署类型之后, 点击继续, 生成以下命令, 然后复制下面linux命令
-
在linux服务器上进行执行复制的命令后, 可以看到连通器已经启动成功
-
新建连通器组
将新建的连通器加入连通器组, 连通器组的作用: 批量管理连通器, 在后面应用管理时使用
补充——连通器自启动配置
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
配置应用管理
准备前提
-
新建应用
方式一: 在管理后台-工作台新建第三方应用
方式二: 在钉钉开放平台根据使用场景去创建应用
-
根据需要填写应用信息
-
配置成功后, 自动生成AgentId, AppKey, AppSecret, 用于接口对接时使用
-
发布应用(开发程序完成后使用)
应用发布后, 才能被其他用户看到(这里指在自己的工作台上看到, 而不是管理后台的工作台上看到). 体验版本发布属于灰度发布.点击查看应用发布介绍
配置应用
-
创建好应用之后, 在应用管理里面配置测试应用, 点击未配置的应用,进行相关配置
-
应用管理 - 基础配置
点击连通器选项的“+”,选择部署的连通器(或连通器组),与测试应用建立连接.
选择域名进行配置, 这里的域名值得是内网地址+端口(可以配置多条映射). 配置好保存之后即可实现内网穿透!!!
-
需要说明的是, 这里的主域名配置就是你在钉钉开放平台->具体应用->开发管理的应用首页地址. 如果后面配置规则之后访问出现
unexpected EOF
, 则说明配置的url在连通器上面访问不通, 一般情况就是在开发配置中, 配置的是https但实际上内网走的是http
例如我上图配置的内网域名是https, 但是实际上curl https://ip:port
则会出错. 说明该服务在内网本身是通过http访问的, 但是在网关域名中我误写成https导致网关穿透失败(因为该域名本身在内网都访问不通!!!)
因此在钉钉开放平台->具体应用->开发管理的应用首页地址之后访问不通时, 一定要验证该url在钉钉连通器所在服务器是否可以访问!
高级配置
注意:
- 下面介绍下高级配置. 如无特殊需求可以不进行高级配置(直接进行下一步).
- 这里配置需的前提是 要我们在下一步访问策略配置并获取外网访问域名后 再进行配置
因为背景介绍的原因, 需要钉钉开放平台在创建的应用中进行事件回调(在公司组织架构进行变动时调用该接口),
然而在我们配置好访问策略时, 钉钉回调接口时仍出现:url 地址访问异常, 不允许3xxx跳转
故需要我们管理平台-应用管理中进行高级配置. 没有配置前, 填入url后报错, 内容如下图所示
- 高级配置
添加允许匿名访问url: 格式为https://生成的外网访问域名/事件回调sdk接口地址.*
- 配置成功后, 进行测试
需要先将事件回调sdk部署在内网服务器上才能够进行测试
配置访问策略
策略管理由企业管理员操作,包括注册策略、修改策略、删除策略、停用策略、启用策略、优先级排序等
- 然后点击“创建策略”按钮,进入创建策略界面
- 在策略页面,填写策略信息,配置项如下表
- 点击完成,创建策略
获取外网访问域名(踩坑)
-
在配置好访问策略后, 可以通过应用管理里面获取新域名, 而不是通过连通器显示的公网ip进行访问
-
可以看到我们每次配置的内网ip+端口都被映射成外网对应可以访问的唯一域名, 根据需要复制新域名即可
-
测试企业网关内网穿透效果
内网访问
公网访问
附: 钉钉事件回调sdk
事件回调介绍地址: https://open.dingtalk.com/document/org/configure-event-subcription
-
钉钉开放平台加解密方法
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)); } }
-
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; }
-
钉钉提供的事件回调列表
其他类型请点击此蓝色链接获取
补充: 前端联调时, 需要为前端配置步骤
在复用钉钉登录功能之后, 前端进行本地调试时, 会进行网关内网穿透, 以便外网可测试访问.
当然也可以直接构造内网的二维码登录url, 但这里我们着重讨论第一种情况. 探究其解决方法
-
在钉钉企业应用网关平台, 配置对应应用的内网ip+端口到外网的映射
-
找到并复制映射好的外网映射
-
在钉钉开放平台的企业应用开发中, 选择自己的应用. 配置登录回调
这里直接配置内网地址, 或者上一步映射好的外网地址均可
如果本步骤没有进行配置, 前端在访问该页面则会出现下面问题
2023-01-03
补充: 如何配置网关连通器开机自启动
ubuntu
整体思路是按照这个大佬的博客走 Ubuntu设置自启动软件 本人使用的服务器是ubuntu 18.04
-
第一步:检查系统目录/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
-
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 ```
-
创建/etc/rc.local脚本文件,并写入想要运行的脚本命令
vim /etc/rc.local
-
给rc.local执行的权限
sudo chmod +x /etc/rc.local
-
启用服务
sudo systemctl enable rc-local sudo systemctl start rc-local.service sudo systemctl status rc-local.service
-
重启电脑看效果
通过重启后查看脚本启动的应用是否能够正常运行
centos操作系统
参考 Linux设置开机自启动的三种方法
-
根据系统文件配置, 找到对应启动脚本, 系统文件所在位置
/lib/systemd/system/rc-local.service
-
编辑启动脚本
vim /etc/rc.d/rc.local
-
授予脚本可执行权限 chmod +x /etc/rc.d/rc.local
-
重启后测试软件是否启动
最后
以上就是长情洋葱为你收集整理的钉钉企业应用网关接入(保姆级教程)背景企业应用网关搭建和踩坑过程附: 钉钉事件回调sdk的全部内容,希望文章能够帮你解决钉钉企业应用网关接入(保姆级教程)背景企业应用网关搭建和踩坑过程附: 钉钉事件回调sdk所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复