diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java index b2cc778ac..030478e53 100644 --- a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java @@ -38,6 +38,10 @@ public class WxCpProperties { * 微信企业号应用 EncodingAESKey */ private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; /** * 微信企业号应用 会话存档类库路径 */ diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java index cfcb16fe0..c4bc30036 100644 --- a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java @@ -18,7 +18,8 @@ public abstract class AbstractWxCpConfigStorageConfiguration { String token = properties.getToken(); Integer agentId = properties.getAgentId(); String aesKey = properties.getAesKey(); - // 企业微信,会话存档路径 + // 企业微信,私钥,会话存档路径 + String msgAuditPriKey = properties.getMsgAuditPriKey(); String msgAuditLibPath = properties.getMsgAuditLibPath(); config.setCorpId(corpId); @@ -32,6 +33,9 @@ public abstract class AbstractWxCpConfigStorageConfiguration { if (StringUtils.isNotBlank(aesKey)) { config.setAesKey(aesKey); } + if (StringUtils.isNotBlank(msgAuditPriKey)) { + config.setMsgAuditPriKey(msgAuditPriKey); + } if (StringUtils.isNotBlank(msgAuditLibPath)) { config.setMsgAuditLibPath(msgAuditLibPath); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java index 63389aeb8..95f484675 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java @@ -34,24 +34,26 @@ public interface WxCpMsgAuditService { * 获取解密的聊天数据Model * * @param chatData getChatDatas()获取到的聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... * @return 解密后的聊天数据 * @throws Exception */ - WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception; + WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; /** * 获取解密的聊天数据明文 * * @param chatData getChatDatas()获取到的聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... * @return 解密后的明文 * @throws Exception */ - String getChatPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception; + String getChatPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; /** * 获取媒体文件 * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 - * + *

* 注意: * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。 * 详情可以看官方文档,亦可阅读此接口源码。 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java new file mode 100644 index 000000000..2e97bf075 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java @@ -0,0 +1,60 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.school.WxCpCustomizeHealthInfo; +import me.chanjar.weixin.cp.bean.school.WxCpResultList; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 企业微信家校应用 复学码相关接口. + * https://developer.work.weixin.qq.com/document/path/93744 + *

+ * 权限说明: + * 仅复学码应用可以调用 + * + * @author Wang_Wong + * @date: 2022/5/31 9:10 + */ +public interface WxCpSchoolService { + + /** + * 获取老师健康信息 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/user/get_teacher_customize_health_info?access_token=ACCESS_TOKEN + * + * @param date + * @param nextKey + * @param limit + * @return + * @throws WxErrorException + */ + WxCpCustomizeHealthInfo getTeacherCustomizeHealthInfo(@NotNull String date, String nextKey, Integer limit) throws WxErrorException; + + /** + * 获取学生健康信息 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/user/get_student_customize_health_info?access_token=ACCESS_TOKEN + * + * @param date + * @param nextKey + * @param limit + * @return + * @throws WxErrorException + */ + WxCpCustomizeHealthInfo getStudentCustomizeHealthInfo(@NotNull String date, String nextKey, Integer limit) throws WxErrorException; + + /** + * 获取师生健康码 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get_health_qrcode?access_token=ACCESS_TOKEN + * + * @param userIds + * @param type + * @return + * @throws WxErrorException + */ + WxCpResultList getHealthQrCode(@NotNull List userIds, @NotNull Integer type) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index 00be57b10..32606b205 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -400,6 +400,13 @@ public interface WxCpService extends WxService { */ WxCpOaService getOaService(); + /** + * 获取家校应用复学码相关接口的服务类对象 + * + * @return + */ + WxCpSchoolService getSchoolService(); + /** * 获取家校应用健康上报的服务类对象 * diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java index 890253b11..fbfdbf383 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java @@ -49,6 +49,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH private WxCpTagService tagService = new WxCpTagServiceImpl(this); private WxCpAgentService agentService = new WxCpAgentServiceImpl(this); private WxCpOaService oaService = new WxCpOaServiceImpl(this); + private WxCpSchoolService schoolService = new WxCpSchoolServiceImpl(this); private WxCpSchoolHealthService schoolHealthService = new WxCpSchoolHealthServiceImpl(this); private WxCpLivingService livingService = new WxCpLivingServiceImpl(this); private WxCpOaAgentService oaAgentService = new WxCpOaAgentServiceImpl(this); @@ -494,6 +495,11 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH return oaService; } + @Override + public WxCpSchoolService getSchoolService() { + return schoolService; + } + @Override public WxCpSchoolHealthService getSchoolHealthService() { return schoolHealthService; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java index 33465921c..fa802a1c6 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java @@ -97,6 +97,7 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { Finance.FreeSlice(slice); WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content); if (chatDatas.getErrCode().intValue() != 0) { + Finance.DestroySingletonSDK(sdk); throw new WxErrorException(chatDatas.toJson()); } @@ -104,23 +105,33 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { } @Override - public WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception { - String plainText = this.decryptChatData(chatData); + public WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception { + String plainText = this.decryptChatData(chatData, pkcs1); return WxCpChatModel.fromJson(plainText); } - public String decryptChatData(WxCpChatDatas.WxCpChatData chatData) throws Exception { - // 企业获取的会话内容将用公钥加密,企业可用自行保存的私钥解开会话内容数据,aeskey不能为空 - String priKey = cpService.getWxCpConfigStorage().getAesKey(); + public String decryptChatData(WxCpChatDatas.WxCpChatData chatData, Integer pkcs1) throws Exception { + /** + * 企业获取的会话内容,使用企业自行配置的消息加密公钥进行加密,企业可用自行保存的私钥解开会话内容数据。 + * msgAuditPriKey 会话存档私钥不能为空 + */ + String priKey = cpService.getWxCpConfigStorage().getMsgAuditPriKey(); if (StringUtils.isEmpty(priKey)) { - throw new WxErrorException("请配置会话存档私钥【aesKey】"); + throw new WxErrorException("请配置会话存档私钥【msgAuditPriKey】"); } - String decryptByPriKey = WxCpCryptUtil.decryptByPriKey(chatData.getEncryptRandomKey(), priKey); - // 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 + String decryptByPriKey = WxCpCryptUtil.decryptPriKey(chatData.getEncryptRandomKey(), priKey, pkcs1); + /** + * 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 + */ long sdk = Finance.SingletonSDK(); long msg = Finance.NewSlice(); + /** + * 解密会话存档内容 + * sdk不会要求用户传入rsa私钥,保证用户会话存档数据只有自己能够解密。 + * 此处需要用户先用rsa私钥解密encrypt_random_key后,作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。 + */ int ret = Finance.DecryptData(sdk, decryptByPriKey, chatData.getEncryptChatMsg(), msg); if (ret != 0) { Finance.FreeSlice(msg); @@ -128,15 +139,17 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { throw new WxErrorException("msg err ret " + ret); } - // 明文 + /** + * 明文 + */ String plainText = Finance.GetContentFromSlice(msg); Finance.FreeSlice(msg); return plainText; } @Override - public String getChatPlainText(WxCpChatDatas.@NonNull WxCpChatData chatData) throws Exception { - return this.decryptChatData(chatData); + public String getChatPlainText(WxCpChatDatas.@NonNull WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception { + return this.decryptChatData(chatData, pkcs1); } @Override diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java new file mode 100644 index 000000000..329c92406 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java @@ -0,0 +1,67 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpSchoolService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.school.WxCpCustomizeHealthInfo; +import me.chanjar.weixin.cp.bean.school.WxCpResultList; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.School.*; + +/** + * 企业微信家校应用 复学码相关接口实现类. + * https://developer.work.weixin.qq.com/document/path/93744 + * + * @author Wang_Wong + * @date: 2022/6/1 14:05 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpSchoolServiceImpl implements WxCpSchoolService { + + private final WxCpService cpService; + + @Override + public WxCpCustomizeHealthInfo getTeacherCustomizeHealthInfo(@NotNull String date, String nextKey, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_TEACHER_CUSTOMIZE_HEALTH_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", date); + jsonObject.addProperty("limit", Optional.ofNullable(limit).orElse(100)); + if (nextKey != null) { + jsonObject.addProperty("next_key", nextKey); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpCustomizeHealthInfo.fromJson(responseContent); + } + + @Override + public WxCpCustomizeHealthInfo getStudentCustomizeHealthInfo(@NotNull String date, String nextKey, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_STUDENT_CUSTOMIZE_HEALTH_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", date); + jsonObject.addProperty("limit", Optional.ofNullable(limit).orElse(100)); + if (nextKey != null) { + jsonObject.addProperty("next_key", nextKey); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpCustomizeHealthInfo.fromJson(responseContent); + } + + @Override + public WxCpResultList getHealthQrCode(@NotNull List userIds, @NotNull Integer type) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_HEALTH_QRCODE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("type", type); + jsonObject.addProperty("userids", userIds.toString()); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpResultList.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpCustomizeHealthInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpCustomizeHealthInfo.java new file mode 100644 index 000000000..a28c3fa35 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpCustomizeHealthInfo.java @@ -0,0 +1,150 @@ +package me.chanjar.weixin.cp.bean.school; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 获取健康信息. + * + * @author Wang_Wong + */ +@Data +public class WxCpCustomizeHealthInfo extends WxCpBaseResp implements Serializable { + private static final long serialVersionUID = -5028321625142879581L; + + @SerializedName("health_infos") + private List healthInfos; + + @SerializedName("template_id") + private String templateId; + + @SerializedName("next_key") + private String nextKey; + + @SerializedName("ending") + private Integer ending; + + @Getter + @Setter + public static class HealthInfo implements Serializable { + private static final long serialVersionUID = -5696099236344075582L; + + @SerializedName("userid") + private String userId; + + @SerializedName("health_qrcode_status") + private Integer healthQrCodeStatus; + + @SerializedName("self_submit") + private Integer selfSubmit; + + @SerializedName("report_values") + private List reportValues; + + @SerializedName("question_templates") + private List questionTemplates; + + public static HealthInfo fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, HealthInfo.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + } + + @Getter + @Setter + public static class ReportValue implements Serializable { + private static final long serialVersionUID = -5696099236344075582L; + + @SerializedName("question_id") + private Integer questionId; + + @SerializedName("single_chose") + private Integer singleChose; + + @SerializedName("text") + private String text; + + public static ReportValue fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, ReportValue.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + } + + @Getter + @Setter + public static class QuestionTemplate implements Serializable { + private static final long serialVersionUID = -5696099236344075582L; + + @SerializedName("question_id") + private Integer questionId; + + @SerializedName("question_type") + private Integer questionType; + + @SerializedName("title") + private String title; + + @SerializedName("is_must_fill") + private Integer isMustFill; + + @SerializedName("is_not_display") + private Integer isNotDisplay; + + @SerializedName("option_list") + private List optionList; + + public static QuestionTemplate fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, QuestionTemplate.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + } + + @Getter + @Setter + public static class OptionList implements Serializable { + private static final long serialVersionUID = -5696099236344075582L; + + @SerializedName("option_id") + private Integer optionId; + + @SerializedName("option_text") + private String optionText; + + public static OptionList fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, OptionList.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + } + + public static WxCpCustomizeHealthInfo fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpCustomizeHealthInfo.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpResultList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpResultList.java new file mode 100644 index 000000000..c4305264d --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/school/WxCpResultList.java @@ -0,0 +1,53 @@ +package me.chanjar.weixin.cp.bean.school; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 获取师生健康码. + * + * @author Wang_Wong + */ +@Data +public class WxCpResultList extends WxCpBaseResp implements Serializable { + private static final long serialVersionUID = -5028321625142879581L; + + @SerializedName("result_list") + private List qrCodeList; + + @Setter + @Getter + public static class QrCodeList extends WxCpBaseResp{ + + @SerializedName("userid") + private String userId; + + @SerializedName("qrcode_data") + private String qrCodeData; + + public static QrCodeList fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, QrCodeList.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + } + + public static WxCpResultList fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpResultList.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index 1d7e9685d..10ae05ead 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -174,6 +174,13 @@ public interface WxCpConfigStorage { */ String getAesKey(); + /** + * 企微会话存档私钥 + * + * @return + */ + String getMsgAuditPriKey(); + /** * 获取企微会话存档系统库 绝对路径 * diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index c716eb735..442e437cf 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -43,6 +43,10 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { private volatile String token; private volatile String aesKey; private volatile long expiresTime; + /** + * 会话存档私钥以及sdk路径 + */ + private volatile String msgAuditPriKey; private volatile String msgAuditLibPath; private volatile String oauth2redirectUri; private volatile String httpProxyHost; @@ -257,6 +261,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { return this.aesKey; } + @Override + public String getMsgAuditPriKey() { + return this.msgAuditPriKey; + } + @Override public String getMsgAuditLibPath() { return this.msgAuditLibPath; @@ -294,6 +303,15 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { this.msgAuditLibPath = msgAuditLibPath; } + /** + * 设置会话存档私钥 + * + * @param msgAuditPriKey 会话存档私钥 + */ + public void setMsgAuditPriKey(String msgAuditPriKey) { + this.msgAuditPriKey = msgAuditPriKey; + } + @Override public String getOauth2redirectUri() { return this.oauth2redirectUri; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 89b939e61..662cf226b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -40,6 +40,7 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { private volatile String token; private volatile String aesKey; private volatile Integer agentId; + private volatile String msgAuditPriKey; private volatile String msgAuditLibPath; private volatile String oauth2redirectUri; private volatile String httpProxyHost; @@ -321,6 +322,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { return this.aesKey; } + @Override + public String getMsgAuditPriKey() { + return this.msgAuditPriKey; + } + @Override public String getMsgAuditLibPath() { return this.msgAuditLibPath; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java index 93246c526..a9528929f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java @@ -177,6 +177,10 @@ public interface WxCpApiPathConsts { String GET_HEALTH_REPORT_STAT = "/cgi-bin/health/get_health_report_stat"; String GET_REPORT_JOBIDS = "/cgi-bin/health/get_report_jobids"; String GET_REPORT_JOB_INFO = "/cgi-bin/health/get_report_job_info"; + + String GET_TEACHER_CUSTOMIZE_HEALTH_INFO = "/cgi-bin/school/user/get_teacher_customize_health_info"; + String GET_STUDENT_CUSTOMIZE_HEALTH_INFO = "/cgi-bin/school/user/get_student_customize_health_info"; + String GET_HEALTH_QRCODE = "/cgi-bin/school/user/get_health_qrcode"; } interface Living { diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java index d36a1ce34..7b09bf4ad 100755 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java @@ -2,14 +2,18 @@ package me.chanjar.weixin.cp.util.crypto; import com.google.common.base.CharMatcher; import com.google.common.io.BaseEncoding; +import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.crypto.WxCryptUtil; import me.chanjar.weixin.cp.config.WxCpConfigStorage; -import org.apache.commons.codec.binary.Base64; +import sun.security.util.DerInputStream; +import sun.security.util.DerValue; import javax.crypto.Cipher; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.Base64; public class WxCpCryptUtil extends WxCryptUtil { public WxCpCryptUtil(WxCpConfigStorage wxCpConfigStorage) { @@ -28,29 +32,77 @@ public class WxCpCryptUtil extends WxCryptUtil { } /** - * 会话存档接口解密私钥 - * 企业获取的会话内容将用公钥加密,企业用自行保存的私钥解开会话内容数据 + * 判断使用PKCS8或者PKCS1进行解密 + * + * @param encryptRandomKey 使用PUBLICKEY_VER指定版本的公钥进行非对称加密后base64加密的内容 + * @param msgAuditPriKey 会话存档私钥 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return + * @throws Exception + */ + public static String decryptPriKey(String encryptRandomKey, String msgAuditPriKey, Integer pkcs1) throws Exception { + if (pkcs1 == null) { + throw new WxErrorException("请配置会话存档解密方式"); + } + + if (pkcs1.intValue() == 1) { + return decryptPriKeyByPKCS1(encryptRandomKey, msgAuditPriKey); + } + + return decryptPriKeyByPKCS8(encryptRandomKey, msgAuditPriKey); + } + + /** + * PKCS8 解密私钥 * * @param encryptRandomKey * @param msgAuditPriKey * @return * @throws Exception */ - public static String decryptByPriKey(String encryptRandomKey, String msgAuditPriKey) throws Exception { + public static String decryptPriKeyByPKCS8(String encryptRandomKey, String msgAuditPriKey) throws Exception { String privateKey = msgAuditPriKey.replaceAll("\\n", "") .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll(" ", ""); - byte[] keyByte = Base64.decodeBase64(privateKey); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyByte); + byte[] keyBytes = Base64.getDecoder().decode(privateKey); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec); Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, priKey); - byte[] utf8 = cipher.doFinal(Base64.decodeBase64(encryptRandomKey)); + byte[] utf8 = cipher.doFinal(Base64.getDecoder().decode(encryptRandomKey)); + return new String(utf8, "UTF-8"); + } + /** + * 会话存档,PKCS1 解密私钥 + * 企业获取的会话内容将用公钥加密,企业用自行保存的私钥解开会话内容数据 + * + * @param encryptRandomKey 使用PUBLICKEY_VER指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。String类型 + * @param msgAuditPriKey 会话存档私钥 + * @return + * @throws Exception + */ + public static String decryptPriKeyByPKCS1(String encryptRandomKey, String msgAuditPriKey) throws Exception { + String privateKey = msgAuditPriKey.replaceAll("\\n", "") + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replaceAll(" ", ""); + + byte[] keyBytes = Base64.getDecoder().decode(privateKey); + DerValue[] seq = new DerInputStream(keyBytes).getSequence(0); + RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(seq[1].getBigInteger(), seq[2].getBigInteger(), + seq[3].getBigInteger(), seq[4].getBigInteger(), + seq[5].getBigInteger(), seq[6].getBigInteger(), + seq[7].getBigInteger(), seq[8].getBigInteger()); + + PrivateKey priKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, priKey); + byte[] utf8 = cipher.doFinal(Base64.getDecoder().decode(encryptRandomKey)); return new String(utf8, "UTF-8"); } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java index 47e511be7..a07db2edf 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java @@ -1,6 +1,5 @@ package me.chanjar.weixin.cp.api; import com.google.common.collect.Lists; - import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; import me.chanjar.weixin.cp.bean.msgaudit.*; @@ -18,7 +17,7 @@ import java.util.List; * 企业微信会话内容存档测试类. * 官方文档:https://developer.work.weixin.qq.com/document/path/91360 * - * @author Wang_Wong + * @author Wang_Wong * @date 2022-01-17 */ @Slf4j @@ -28,6 +27,7 @@ public class WxCpMsgAuditTest { private static WxCpService cpService; // com.binarywang.spring.starter.wxjava.cp.config.WxCpServiceAutoConfiguration + // WxCpServiceImpl.getAccessToken() @Test public void test() throws Exception { @@ -39,20 +39,26 @@ public class WxCpMsgAuditTest { cpService.setWxCpConfigStorage(config); /** - * 配置: + * 仔细配置: * - * wwa3bexxXXXXXX - * 自定义agentId - * xIpum7Yt4NMXXXXXXX - * 2bSNqXXXXXXXX - * MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZuPVMyVyMvJkdSCZA893B2pggd1r95T8k2QZgz+VejtaDJCbD60mYoW1Uwwlwuqy8W78M6MmXsskn+5XunyR1WJlJGqgi0OMVGYvSfkNb9kD50fM21CGLcN1y4miL9fVNBIsvJmIUeJCNS8TioAVGFvh2EgzjqTR1gHfDwu8If7rfGmTHkPYL8hQxMg/EQ3451JOIBHSa7EQSx64SIoVWEgSDFQjGEpjUiJRfciyyz+nTSkEDgFa9hpyTS6E0c/3Q5lVDFgIwTArC19XBFKb00PbcFuLriOIsTBX4K9XWBtefVXowAdqUVQDH6BNUIK7/iVPQ4L3p+F5DBOrx8I/7AgMBAAECggEBAK53C/nwEX2lU3ynaB/8SuMga274ta1mmmbIkdfaQA65nyOPQJEWZe8szBN0BoiSzgBR9JI/p+srlQ25CLgiRnDSAmMWPU1I3e72fZi7HPcAKakGmEKDUi4OzyVUUDp3aY3B6lZqB4Yn5o2S/b4sRI2ZspfKdxGncSYHP/Far3i6hzq2C1hbyYM6HkHPcrQ+z6ir6GxjLvHXssVJ+/C0HMsVIQAWPyEGbzWozS+EswmQ+itk+7cewiLWbaCSp6lsjHKGTxJwCxRes0nUt2SfkLnIUkDLxB7c6zDQJCn1K2UckCjNBlCWl+oDWLkLQ7UAJ+4IYYSslR4wXzRg8PplW8ECgYEA9VlEprEoG2oSn3HXIMFg0MANViQe89QJQdwd7D5h4FLxXQLItxqmZj77iktlzlICcK9WT9WHRY1AOilsuMaDmY0VH3Z8r/X9BU712KFJqMYH5CNxrqHOya3BG+CclEKToaOTmo9kiOpFAMNSuuWs6gvILJ0CKEmSUo5G9fJu4fkCgYEA4yypHoRZIP0mDdVDeVtdHHcq5JdWF6xbAFs4P57VHG1KDMWouk3IHSeO279gEIwcBAdaLcMMgFfzyQBwcisxjC76oyoZnbSntB7ZMFdPqALKfxIdleLilbASuRKesVAF+OgOx/yp/aQUeLG2pVBivgn2TyGMwjnxznTh9vh+vpMCgYEAmOva7krdRLkIgnjiLXhab8JEjbxVzoQKgRJBVE5NkxQffGmP0RC7Rl9bSQdVnRNgkfu3QGtGtQMlVRscuM6Cl+JnmASyErqvye89LJja4GcN5BRzdvVDflDeXBHThlU4zza1eVCGyQ+7ko4rsnIVJIvTaHs0LQguO2aStBk3I4ECgYAyBsO3VK3L9fNLWItjThtTCWsIq8rpq6reiTf5yqBjgi2sYlqlrDtFMFDlU190RWZl/Lh/G1TFbpjgypf4jEp89Ft9UugRMpc7sw9g9dk0xmiRUwvw1eXP0NZOqysHIPgvt+qJX7qPgHKBoaD3Bpy3/Lmg82Jr4xa8wECCgnZmwQKBgH7hirPs1/HqBrbxS726IZUf9QTmVkyOYIwzuwFYKb/+4caSah+iaXexVux0xS5tchj/6c1dQSKJmlegV8smIb6EEcko7llA1y1P5QFtXtaaRd07tTsv3BKEg496YLRjbxPzgJn6Fsoz3TTdGwESL8Q3I2h0WmVVhmr/rjr+RkWQ + * ww45xxx88865xxx + * xIpum7Yt4NMXcyxdzcQ2l_46BG4QIQDR57MhA45ebIw // secret + * 200000 // 会话存档的应用id + * // 回调配置的token + * // 回调配置的EncodingAESKey + * + * // 企业微信会话存档 + * // 1、会话存档私钥,一定要加上前缀!! + * // 2、仔细配置windows以及linux环境sdk路径 + * MIxxx893B2pggd1r95T8k2QxxxxbD6xxxxmXsskn+5XunyR1WJlJGqgi0OMVGYvSfkNb9kD50fM21CGLcN1y4miL9fVNBIsvJmIUeJCNS8TioAVGFvh2EgzjqTR1gH + * /www/osfile/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so + * * * 注意:最好先配置lib开头的系统库,再配置sdk类库,配置绝对路径,最好配置为linux路径 - * windows: - * D:/WorkSpace/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so - * linux: - * /www/osfile/work_msg_storage/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so - * + * Windows: + * D:/WorkSpace/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so + * Linux: + * /www/osfile/work_msg_storage/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so */ /** @@ -84,12 +90,13 @@ public class WxCpMsgAuditTest { // Integer publickeyVer = chatData.getPublickeyVer(); // 获取明文数据 - final String chatPlainText = cpService.getMsgAuditService().getChatPlainText(chatData); + final String chatPlainText = cpService.getMsgAuditService().getChatPlainText(chatData, 2); final WxCpChatModel wxCpChatModel = WxCpChatModel.fromJson(chatPlainText); log.info("明文数据为:{}", wxCpChatModel.toJson()); // 获取消息数据 - final WxCpChatModel decryptData = cpService.getMsgAuditService().getDecryptData(chatData); + // https://developer.work.weixin.qq.com/document/path/91774 + final WxCpChatModel decryptData = cpService.getMsgAuditService().getDecryptData(chatData, 2); log.info("获取消息数据为:{}", decryptData.toJson()); /** @@ -435,6 +442,16 @@ public class WxCpMsgAuditTest { WxCpGroupChat room = cpService.getMsgAuditService().getGroupChat("wrOQpTDwAAyPl84GBJ40W5eWxWtixSCA"); log.info(room.toJson()); + + /** + * 获取access_token + * https://developer.work.weixin.qq.com/document/path/91039 + * https://www.jianshu.com/p/dde171887d63 + */ + String getUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"; + String data = cpService.get(String.format(getUrl, config.getCorpId(), config.getCorpSecret()), null); + + } } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpSchoolTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpSchoolTest.java new file mode 100644 index 000000000..ddb1d47c0 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpSchoolTest.java @@ -0,0 +1,72 @@ +package me.chanjar.weixin.cp.api; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; +import me.chanjar.weixin.cp.bean.school.WxCpCustomizeHealthInfo; +import me.chanjar.weixin.cp.bean.school.WxCpResultList; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.demo.WxCpDemoInMemoryConfigStorage; +import org.testng.annotations.Test; + +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +/** + * 企业微信家校应用 复学码相关接口. + * https://developer.work.weixin.qq.com/document/path/93744 + * + * @author Wang_Wong + * @date: 2022/5/31 9:10 + */ +@Slf4j +public class WxCpSchoolTest { + + private static WxCpConfigStorage wxCpConfigStorage; + private static WxCpService cpService; + + @Test + public void test() throws WxErrorException { + + /** + * 注意: + * 权限说明:仅复学码应用可以调用 + */ + InputStream inputStream = ClassLoader.getSystemResourceAsStream("test-config.xml"); + WxCpDemoInMemoryConfigStorage config = WxCpDemoInMemoryConfigStorage.fromXml(inputStream); + + wxCpConfigStorage = config; + cpService = new WxCpServiceImpl(); + cpService.setWxCpConfigStorage(config); + String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + + + /** + * 获取老师健康信息 + * https://developer.work.weixin.qq.com/document/path/93744 + */ + WxCpCustomizeHealthInfo teacherCustomizeHealthInfo = cpService.getSchoolService().getTeacherCustomizeHealthInfo(date, null, null); + log.info("teacherCustomizeHealthInfo为:{}", teacherCustomizeHealthInfo.toJson()); + + /** + * 获取学生健康信息 + * https://developer.work.weixin.qq.com/document/path/93745 + */ + WxCpCustomizeHealthInfo studentCustomizeHealthInfo = cpService.getSchoolService().getStudentCustomizeHealthInfo(date, null, null); + log.info("studentCustomizeHealthInfo为:{}", studentCustomizeHealthInfo.toJson()); + + /** + * 获取师生健康码 + * https://developer.work.weixin.qq.com/document/path/93746 + */ + ArrayList userIds = Lists.newArrayList(); + userIds.add("Wangkai"); + WxCpResultList healthQrCode = cpService.getSchoolService().getHealthQrCode(userIds, 1); + log.info("healthQrCode为:{}", healthQrCode.toJson()); + + } + +} diff --git a/weixin-java-cp/src/test/resources/test-config.sample.xml b/weixin-java-cp/src/test/resources/test-config.sample.xml index 23e83e942..19241aba7 100644 --- a/weixin-java-cp/src/test/resources/test-config.sample.xml +++ b/weixin-java-cp/src/test/resources/test-config.sample.xml @@ -11,6 +11,18 @@ 企业号通讯录里的某个tagid 网页授权获取用户信息回调地址 webhook链接地址的key值 - + + -----BEGIN RSA PRIVATE KEY----- + MIICXAIBAAKBgQCTfm5cxqfglfOV7b/Z7OtTZZoZpk2EPTvVhn/ngsfKR899xRdR + 25s4h8HkK0XhxqYdOGoAdG3Gms+DvCSY/vu3UtImf0eZSNXpKZJBUnvUVjX4ivnr + Ohu2Rjw6O4gPjPnZKw8voCu0Nae1YLeNvFYw48PK7QrqmpHQv1sCd/8zHwIDAQAB + AoGAResz7xgfQghjsgnEHk9BGUY7YHhlG9CZWjYJ0Ro+ksYq9vClBuGHeitk/0CC + Pq7YVVbGbVPELFd8EvNwF/UcJsMlvFis16FzNS60Hn7M/o82gI6AVhSQmocoGhNs + MIKxTnXRqqlKFbCdcSfG+hQP7syHah6Z8UhLYuEA8s/ppd0CQQD99HTSvB4P5FfL + rlKTz6w6uh4qBYl53u5cLQxCRFGgXD6HvPnEwdzQf+2LCVM1zIhyxw2Kak1U467Q + 6JizEuHDTC2YljEbg/j+/AlpA/Ua5HQYnH5yD3DCK7rQyTvqE5gU+CfRbwTbLGre + fk/WJK4iqizgZobNRyUCQGB7jR5b8K7NsX7SoV7v/PFOsoj4G2W5q7LSz4GaoXGl + 3F+dSlXPYHdTow3dzfgVDldEzgoThs5UWMTQvBUZch0= + -----END RSA PRIVATE KEY----- /www/osfile/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,libWeWorkFinanceSdk_Java.so