Browse Source

钉钉事件注册

hanzhuoyue 10 months ago
parent
commit
ce3afd26c0

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingding/CallbackConstant.java

@@ -0,0 +1,61 @@
+package com.ruoyi.web.controller.dingding;
+
+/**
+ * 钉钉回调相关事件类型
+ *
+ * @author lnexin
+ */
+public class CallbackConstant {
+    /**
+     * 回调时成功的字符串
+     */
+    public static final String CALLBACK_RETURN_SUCCESS = "success";
+    /**
+     * 激活失败返回
+     */
+    public static final String ACTIVE_RETURN_FAILURE = "active_failure";
+
+    /**
+     * 验证回调地址是否有效
+     */
+    public static final String SUITE_TICKET_CALLBACK_URL_VALIDATE = "suite_ticket";
+    /**
+     * 在开发者后台修改套件时如果回调地址有变化会推送该事件
+     */
+    public static final String CHECK_UPDATE_SUITE_URL = "check_update_suite_url";
+    /**
+     * 钉钉向回调URL POST数据, 解密后是否成功
+     */
+    public static final String CHECK_CREATE_SUITE_URL = "check_create_suite_url";
+
+    /**
+     * 临时授权码,授权开通
+     */
+    public static final String TEMP_AUTH_CODE_ACTIVE = "tmp_auth_code";
+    /**
+     * 解除授权事件
+     */
+    public static final String SUITE_RELIEVE = "suite_relieve";
+
+    /**
+     * 停用应用
+     */
+    public static final String ORG_MICRO_APP_STOP = "org_micro_app_stop";
+    /**
+     * 启用应用
+     */
+    public static final String ORG_MICRO_APP_RESTORE = "org_micro_app_restore";
+
+    /**
+     * 授权方(即授权企业)在钉钉手机客户端-微应用管理中,修改了对应用的授权企业通讯录范围
+     * 授权变更信息并不包括企业用户具体做了什么修改,所以收到推送之后,ISV需要通过调用“获取通讯录权限接口”查询新的授权范围。。
+     */
+    public static final String CONTACT_CHANGE_AUTH = "change_auth";
+
+    /**
+     * 用户下单购买事件
+     */
+    public static final String MARKET_BUY = "market_buy";
+
+
+}

+ 118 - 36
ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingding/DingAuthController.java

@@ -20,6 +20,7 @@ import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
 import com.dingtalk.open.app.api.message.GenericOpenDingTalkEvent;
 import com.dingtalk.open.app.api.security.AuthClientCredential;
 import com.dingtalk.open.app.stream.protocol.event.EventAckStatus;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.entity.SysRole;
@@ -44,18 +45,17 @@ import com.taobao.api.ApiException;
 import com.taobao.api.TaobaoResponse;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 
 import static com.ruoyi.framework.datasource.DynamicDataSourceContextHolder.log;
+import static com.ruoyi.web.controller.dingding.CallbackConstant.*;
 
 /**
  * <p>DingLoginController 此类用于:钉钉企业内部应用免登(H5微应用)</p>
@@ -64,6 +64,7 @@ import static com.ruoyi.framework.datasource.DynamicDataSourceContextHolder.log;
 @Api(value = "dingAuthController", tags = "钉钉企业内部应用免登(H5微应用)")
 @Controller
 @RequestMapping(value = "/ding")
+@Slf4j
 public class DingAuthController {
     @Resource
     private DingAppConfig dingAppConfig;
@@ -112,44 +113,100 @@ public class DingAuthController {
      */
     private static final String EVENT_TMP_AUTH_CODE = "tmp_auth_code";
 
-    @PostMapping(value = "dingCallback2")
-    public Object dingCallback2(
-            @RequestParam(value = "signature") String signature,
-            @RequestParam(value = "timestamp") Long timestamp,
-            @RequestParam(value = "nonce") String nonce,
-            @RequestBody(required = false) JSONObject body
-    ) {
-        String params = "signature:" + signature + " timestamp:" + timestamp + " nonce:" + nonce + " body:" + body;
-        DingTalkEncryptor dingTalkEncryptor = null;
+    @PostMapping(value = "/callback1")
+    public Map<String, String> callback1(@RequestParam String signature,
+                                                    @RequestParam String timestamp,
+                                                    @RequestParam String nonce,
+                                                    @RequestBody JsonNode encryptNode) {
+        String encryptMsg = encryptNode.get("encrypt").textValue();
+        String plainText = this.decryptText(signature, timestamp, nonce, encryptMsg);
+        JSONObject callBackContent = JSON.parseObject(plainText);
+
+        //进入回调事件分支选择
+        Map<String, String> resultMap = caseProcess(callBackContent);
+        return resultMap;
+    }
+    public String decryptText(String signature, String timestamp, String nonce, String encryptMsg) {
+        String plainText = "";
         try {
-            dingTalkEncryptor = new DingTalkEncryptor(Constant.TOKEN, Constant.ENCODING_AES_KEY, Constant.SUITE_KEY);
-            // 从post请求的body中获取回调信息的加密数据进行解密处理,消息加解密参见下文
-            String encrypt = body.getString("encrypt");
-            String plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp.toString(), nonce, encrypt);
-            log.info("应用suite_ticket数据推送: " + plainText);
-            JSONObject callBackContent = JSON.parseObject(plainText);
-            log.info("begin callback encrypt:" + callBackContent.toString());
-            // 根据回调事件类型做不同的业务处理
-            String eventType = callBackContent.getString("EventType");
-            //TODO 将数据持久化
-            return plainText;
-        } catch (Exception e) {
-            log.error("process callback fail." + params, e);
-            return "fail";
+            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(Constant.TOKEN, Constant.ENCODING_AES_KEY, Constant.SUITE_KEY);
+            plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, encryptMsg);
+        } catch (DingTalkEncryptException e) {
+            log.error("钉钉消息体解密错误, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, e: {}", signature, timestamp, nonce, encryptMsg, e);
         }
+        log.debug("钉钉消息体解密, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, 解密结果: {}", signature, timestamp, nonce, encryptMsg, plainText);
+        return plainText;
     }
-    @GetMapping(value = "send")
-    public void send() {
-        try {
 
-        } catch (Exception e) {
-            log.info("start fail."+e.getMessage());
-            throw new RuntimeException(e);
+    private Map<String, String> caseProcess(JSONObject plainNode) {
+        Map<String, String> resultMap = new LinkedHashMap<>();
+        String eventType = plainNode.getString("EventType");
+        switch (eventType) {
+            case SUITE_TICKET_CALLBACK_URL_VALIDATE:
+                log.info("[callback] 验证回调地址有效性质:{}", plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case TEMP_AUTH_CODE_ACTIVE:
+                log.info("[callback] 企业开通授权:{}", plainNode);
+
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case SUITE_RELIEVE:
+                log.info("[callback] 企业解除授权:{}", plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CHECK_UPDATE_SUITE_URL:
+                log.info("[callback] 在开发者后台修改回调地址:" + plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CHECK_CREATE_SUITE_URL:
+                log.info("[callback] 检查钉钉向回调URL POST数据解密后是否成功:" + plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CONTACT_CHANGE_AUTH:
+                log.info("[callback] 通讯录授权范围变更事件:" + plainNode);
+                break;
+            case ORG_MICRO_APP_STOP:
+                log.info("[callback] 停用应用:" + plainNode);
+                break;
+            case ORG_MICRO_APP_RESTORE:
+                log.info("[callback] 启用应用:" + plainNode);
+                break;
+            case MARKET_BUY:
+                log.info("[callback] 用户下单购买事件:" + plainNode);
+                break;
+            default:
+                log.info("[callback] 未知事件: {} , 内容: {}", eventType, plainNode);
+                resultMap = encryptText("事件类型未定义, 请联系应用提供方!" + eventType);
+                break;
+        }
+        return resultMap;
+    }
+    public Map<String, String> encryptText(String text) {
+        Map<String, String> resultMap = new LinkedHashMap<>();
+        try {
+            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(Constant.TOKEN, Constant.ENCODING_AES_KEY, Constant.SUITE_KEY);
+            resultMap = dingTalkEncryptor.getEncryptedMap(text, System.currentTimeMillis(),getRandomStr(8));
+        } catch (DingTalkEncryptException e) {
+            log.error("钉钉消息体加密,text: {}, e: {}", text, e);
         }
+        log.debug("钉钉消息体加密,text: {}, resultMap: {}", text, resultMap);
+        return resultMap;
     }
+    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));
+        }
 
-    @PostMapping(value = "callback")
-    public Object dingCallback(
+        return sb.toString();
+    }
+    @PostMapping(value = "callback2")
+    public Object callback2(
             @RequestParam(value = "signature") String signature,
             @RequestParam(value = "timestamp") Long timestamp,
             @RequestParam(value = "nonce") String nonce,
@@ -195,7 +252,32 @@ public class DingAuthController {
             return "fail";
         }
     }
-
+    @PostMapping(value = "callback3")
+    public Object callback3(
+            @RequestParam(value = "signature") String signature,
+            @RequestParam(value = "timestamp") Long timestamp,
+            @RequestParam(value = "nonce") String nonce,
+            @RequestBody(required = false) JSONObject body
+    ) {
+        String params = "signature:" + signature + " timestamp:" + timestamp + " nonce:" + nonce + " body:" + body;
+        DingTalkEncryptor dingTalkEncryptor = null;
+        try {
+            dingTalkEncryptor = new DingTalkEncryptor(Constant.TOKEN, Constant.ENCODING_AES_KEY, Constant.SUITE_KEY);
+            // 从post请求的body中获取回调信息的加密数据进行解密处理,消息加解密参见下文
+            String encrypt = body.getString("encrypt");
+            String plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp.toString(), nonce, encrypt);
+            log.info("应用suite_ticket数据推送: " + plainText);
+            JSONObject callBackContent = JSON.parseObject(plainText);
+            log.info("begin callback encrypt:" + callBackContent.toString());
+            // 根据回调事件类型做不同的业务处理
+            String eventType = callBackContent.getString("EventType");
+            //TODO 将数据持久化
+            return plainText;
+        } catch (Exception e) {
+            log.error("process callback fail." + params, e);
+            return "fail";
+        }
+    }
     /**
      * 钉钉用户登录,显示当前登录用户的userId和名称
      *

+ 3 - 3
ruoyi-admin/src/main/resources/application.yml

@@ -18,7 +18,7 @@ ruoyi:
 # 开发环境配置
 server:
   # 服务器的HTTP端口,默认为8080
-  port: 8080
+  port: 8093
   servlet:
     # 应用的访问路径
     context-path: /
@@ -72,11 +72,11 @@ spring:
     # 地址
     host: localhost
     # 端口,默认为6379
-    port: 6379
+    port: 6380
     # 数据库索引
     database: 0
     # 密码
-    password:
+    password: 123456
     # 连接超时时间
     timeout: 10s
     lettuce:

+ 1 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -114,7 +114,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 单点登录authSingleSignIn 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/login","/authSingleSignIn","/ding/**","/mobile/**","/system/resume/**","/fg/**", "/register", "/captchaImage").permitAll()
+                .antMatchers("/login","/authSingleSignIn","/prod-api/ding/callback1","/ding/**","/mobile/**","/system/resume/**","/fg/**", "/register", "/captchaImage").permitAll()
                 // 静态资源,可匿名访问
                 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                 .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()