当前位置: 首页 > 工具软件 > JFinal OA > 使用案例 >

接入 钉钉 OA 审批

林炫明
2023-12-01

一,首先区分 钉钉 专有钉钉 浙政钉 是不一样的,我接入的是普通钉钉的OA审批
二,开发文档地址 https://open.dingtalk.com/

1,引入依赖

<dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dingtalk</artifactId>
            <version>1.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

2,在配置文件中 添加配置

OAdd:
  appKey: ***************
  appSecret: ***************
  dingTalk: https://oapi.dingtalk.com/
  processCode: ****************
  originatorUserId: *****************
  callToken: *********************
  callAesKey: *******************

3,编写OA工具类

3.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;

/**
 * DingTalk open platform encryption and decryption method
 * Download JCE Unrestricted Permission Policy File from ORACLE Official Website
 * JDK6 download address: http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
 * JDK7 download address: http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
 * JDK8 download address https://www.oracle.com/java/technologies/javase-jce8-downloads.html
 */

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 fixed length
     **/
    private static final Integer AES_ENCODE_KEY_LENGTH = 43;
    /**
     * Encrypted random string byte length
     **/
    private static final Integer RANDOM_LENGTH = 16;

    /**
     * Constructor
     *
     * @param token On the DingTalk open platform, the token set by the developer
     * @param encodingAesKey The EncodingAESKey set by the developer on the DingTalk open platform
     * @param corpId Enterprise self-built application-event subscription, use appKey
     * Enterprise self-built application - register callback address, use corpId
     * Third-party enterprise applications, use suiteKey
     *
     * @throws DingTalkEncryptException failed to execute, please check the exception's error code and specific error message
     */
    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));
    }

    /**
     * Encrypt the message body synchronized with the DingTalk open platform and return the encrypted Map
     *
     * @param plaintext The message body plaintext passed
     * @param timeStamp timestamp
     * @param nonce random string
     * @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);
        }
        // encrypt
        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;
    }

    /**
     * ciphertext decryption
     *
     * @param msgSignature signature string
     * @param timeStamp timestamp
     * @param nonce random string
     * @param encryptMsg ciphertext
     * @return decrypted original text
     * @throws DingTalkEncryptException
     */
    public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
            throws DingTalkEncryptException {
        //check signature
        String signature = getSignature(token, timeStamp, nonce, encryptMsg);
        if (!signature.equals(msgSignature)) {
            throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
        }
        // decrypt
        String result = decrypt(encryptMsg);
        return result;
    }

    /*
     * Encrypt plaintext.
     * @param text The plaintext to be encrypted
     * @return encrypted base64 encoded string
     */
    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);
        }
    }

    /*
     * Decrypt the ciphertext.
     * @param text The ciphertext to decrypt
     * @return decrypted plaintext
     */
    private String decrypt(String text) throws DingTalkEncryptException {
        byte [] originalArr ;
        try {
            // Set the decryption mode to CBC mode of AES
            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);
            // Decode the ciphertext using BASE64
            byte[] encrypted = Base64.decodeBase64(text);
            // decrypt
            originalArr = cipher.doFinal(encrypted);
        } catch ( Exception  e ) {
            throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
        }

        String plainText;
        String fromCorpid;
        try {
            // remove the complement character
            byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
            // Separate 16-bit random string, network byte order and 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);
        }

        // When the corpids are not the same
        if (!fromCorpid.equals(corpId)) {
            throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
        }
        return plainText;
    }

    /**
     * digital signature
     *
     * @param token     isv token
     * @param timestamp timestamp
     * @param nonce random string
     * @param encrypt encrypted text
     * @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 , " success" ) ;
            msgMap.put ( 900001 , " Illegal encrypted plaintext" ) ;
            msgMap.put ( 900002 , " Illegal encryption timestamp parameter" ) ;
            msgMap.put ( 900003 , " The encrypted random string parameter is illegal" );
            msgMap.put ( 900005 , "Signature mismatch " ) ;
            msgMap.put ( 900006 , " Signature calculation failed" ) ;
            msgMap .put ( 900004 , " Illegal aes key" );
            msgMap.put ( 900007 , " Error computing encrypted text" ) ;
            msgMap.put ( 900008 , " Error computing decrypted text" ) ;
            msgMap.put ( 900009 , " Calculation of decrypted text length does not match" );
            msgMap.put ( 900010 , "Compute decrypted literal corpid does not match" );
        }

        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));
    }
}

3.2 OA工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiGettokenRequest;
import com.dingtalk.api.request.OapiProcessinstanceCreateRequest;
import com.dingtalk.api.request.OapiProcessinstanceGetRequest;
import com.dingtalk.api.response.OapiGettokenResponse;
import com.dingtalk.api.response.OapiProcessinstanceCreateResponse;
import com.dingtalk.api.response.OapiProcessinstanceGetResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.taobao.api.ApiException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * 钉钉OA审批工具类
 * @author HZHP
 */
@Component
public class OaUtils {

    @Value("${OAdd.appKey}")
    private String appKey;
    @Value("${OAdd.appSecret}")
    private String appSecret;

    @Value("${OAdd.dingTalk}")
    private String dingTalk;

    @Value("${OAdd.processCode}")
    private String processCode;

    @Value("${OAdd.originatorUserId}")
    private String originatorUserId;

    private ObjectMapper om=new ObjectMapper();

    /**
     * 获取accessToken  有效期 2小时  频繁访问将限流 建议缓存
     * @return
     * @throws JsonProcessingException
     */
    public String getAeecssToken() throws JsonProcessingException {
        String str="";
        try {
            DingTalkClient client = new DefaultDingTalkClient(dingTalk+"gettoken");
            OapiGettokenRequest req = new OapiGettokenRequest();
            req.setHttpMethod("GET");
            req.setAppkey(appKey);
            req.setAppsecret(appSecret);
            OapiGettokenResponse rsp = client.execute(req);
            str = rsp.getBody();
        } catch (ApiException e) {
            e.printStackTrace();
        }
        HashMap hashMap = om.readValue(str, HashMap.class);
        return hashMap.get("access_token").toString();
    }

    /**
     * 获取审批详情
     * @return
     * @throws ApiException
     */
    public Map<String,String> getProcessInsDetail(String insTanceId, String acceToken) throws Exception {
        Map<String,String> resultMap=new HashMap<>(2);
        DingTalkClient client = new DefaultDingTalkClient(dingTalk+"topapi/processinstance/get");
        OapiProcessinstanceGetRequest req = new OapiProcessinstanceGetRequest();
        req.setProcessInstanceId(insTanceId);
        OapiProcessinstanceGetResponse rsp = client.execute(req, acceToken);
        String body = rsp.getBody();
        HashMap map = om.readValue(body, HashMap.class);
        HashMap pie =(HashMap) map.get("process_instance");
        resultMap.put("status",pie.get("status").toString());
        resultMap.put("result",pie.get("result").toString());
        return resultMap;
    }

    /**
     * 发起审批
     * @param acceToken
     * @return
     * @throws ApiException
     */
    public Map<String,String> createApproval(String acceToken) throws ApiException {
        Map<String,String> result=new HashMap<>(2);
        DingTalkClient client = new DefaultDingTalkClient(dingTalk+ "topapi/processinstance/create");
        OapiProcessinstanceCreateRequest req = new OapiProcessinstanceCreateRequest();
        req.setProcessCode(processCode);
        req.setOriginatorUserId(originatorUserId);
        // 若发起人属于跟部门 传 -1
        req.setDeptId(-1L);

        //单行输入框
        List<OapiProcessinstanceCreateRequest.FormComponentValueVo> formComponentValueVoList = new ArrayList<OapiProcessinstanceCreateRequest.FormComponentValueVo>();
        OapiProcessinstanceCreateRequest.FormComponentValueVo formComponentValueVo = new OapiProcessinstanceCreateRequest.FormComponentValueVo();
        formComponentValueVoList.add(formComponentValueVo);
        formComponentValueVo.setName("备注");
        formComponentValueVo.setValue("代码创建审批测试");

        req.setFormComponentValues(formComponentValueVoList);
        OapiProcessinstanceCreateResponse rsp = client.execute(req, acceToken);
        String str = JSON.toJSONString(rsp);
        JSONObject eventJson = JSON.parseObject(str);
        result.put("errcode",eventJson.get("errcode").toString());
        result.put("processInstanceId",eventJson.get("processInstanceId").toString());
        return result;
    }
}

3.3 事件回调类

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zjhcsoft.bi.jzbf.utils.OaUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson.JSON;

import java.util.Map;

@RestController
@RequestMapping("approval")
public class ApprovalResult {

    @Value("${OAdd.appKey}")
    private String appKey;
    @Value("${OAdd.callToken}")
    private String callToken;
    @Value("${OAdd.callAesKey}")
    private String callAesKey;

    @Autowired
    private OaUtils oaUtils;

    @PostMapping
    public Map<String, String> approval(@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(callToken, callAesKey, appKey);
            String encryptMsg = json.getString("encrypt");
            String decryptMsg = callbackCrypto.getDecryptMsg(msg_signature, timeStamp, nonce, encryptMsg);

            // 3. 反序列化回调事件json数据
            JSONObject eventJson = JSON.parseObject(decryptMsg);
            String eventType = eventJson.getString("EventType");

            // 4. 根据EventType分类处理
            if ("check_url".equals(eventType)) {
                // 测试回调url的正确性
                System.out.println("测试回调url的正确性");
            } else if ("bpms_instance_change".equals(eventType)) {
                // 处理审批事件 TODO
                String processInstanceId = eventJson.get("processInstanceId").toString();
                String aeecssToken = oaUtils.getAeecssToken();
                Map<String, String> processInsDetail = oaUtils.getProcessInsDetail(processInstanceId, aeecssToken);
                System.out.println("审批事件----------------->响应结果为----------------->");
                System.out.println(aeecssToken);
                System.out.println(processInsDetail);
                System.out.println("审批事件结束----------------------------------------》");
            } else {
                // 添加其他已注册的
                System.out.println("发生了:" + eventType + "事件");
            }

            // 5. 返回success的加密数据
            Map<String, String> successMap = callbackCrypto.getEncryptedMap("success");
            return successMap;

        } catch (DingCallbackCrypto.DingTalkEncryptException | JsonProcessingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
 类似资料: