SYSTEM_CACHE = new HashMap<>();
+
+ /**
+ * 授权地址
+ */
+ private static final String OA_AUTHORIZE_URL = "/sso/oauth2.0/authorize?client_id=%s&response_type=code&redirect_uri=%s";
+ /**
+ * 获取 access_token 接口
+ */
+ private static final String OA_ACCESS_TOKEN_URL = "/sso/oauth2.0/accessToken?client_id=%s&client_secret=%s&grant_type=%s&code=%s&redirect_uri=%s";
+ /**
+ * 获取用户信息接口
+ */
+ private static final String OA_PROFILE_URL = "/sso/oauth2.0/profile?access_token=%s";
+
+ /**
+ * 认证退出接口
+ */
+ private static final String OA_LOGOUT_URL = "/sso/logout?service=%s";
+
+ public static final String USER_API = "/api/hrm/resful/getHrmUserInfoWithPage";
+ public static final String REGISTER_API = "/api/ec/dev/auth/regist";
+ public static final String TOKEN_API = "/api/ec/dev/auth/applytoken";
+ public static final String SEND_MESSAGE_API = "/api/ec/dev/message/sendCustomMessageSingle";
+ public static final String ALTER_MESSAGE_API = "/api/ec/dev/message/alterCustomMessageSingle";
+ public static final String DELETE_MESSAGE_API = "/api/ec/dev/message/ deleteCustomMessageSingle";
+
+
+
+ /**
+ * 错误代码的key
+ */
+ public static final String ERROR_CODE = "code";
+ public static final int NUM_0 = 0;
+ private static final String NUM_1 = "1";
+
+
+ /**
+ * userid的key
+ */
+ public static final String USR_ID = "id";
+
+
+
+ /**
+ * 获取到的凭证,最长为512字节
+ */
+ private static final String ACCESS_TOKEN = "access_token";
+ private static final String GRANT_TYPE = "authorization_code";
+ /**
+ * 魔法值
+ */
+ private static final String APP_ID = "appid";
+ private static final String CPK = "cpk";
+ private static final String SPK = "spk";
+ private static final String SECRIT = "secrit";
+ private static final String SECRET = "secret";
+ private static final int TIME_2000 = 2000;
+ private static final String TIME_3600 = "3600";
+ private static final String LOCAL_PRIVATE_KEY = "LOCAL_PRIVATE_KEY";
+ private static final String LOCAL_PUBLIC_KEY = "LOCAL_PUBLIC_KEY";
+ private static final String SERVER_PUBLIC_KEY = "SERVER_PUBLIC_KEY";
+ private static final String SERVER_SECRET = "SERVER_SECRET";
+ private static final String SERVER_TOKEN = "SERVER_TOKEN";
+ private static final String TOKEN = "token";
+ private static final String TIME_NAME = "time";
+ private static final String SESSION = "skipsession";
+
+ private static final String UTF_8 = "utf-8";
+
+
+ /**
+ * 客户端应用注册ID
+ */
+ private static String CLIENT_ID = "CLIENT_ID";
+
+ /**
+ * ecology系统发放的授权许可证(appid)
+ */
+ private static String APPID = "APPID";
+ private static String CODE = "CODE";
+
+ /**
+ * 客户端应用注册密钥
+ */
+ private static String CLIENT_SECRET = "CLIENT_SECRET";
+
+ /**
+ * 认证中心地址
+ */
+ public static String SERVER_PATH = "";
+
+ @Value("${pmhub.oa.appId}")
+ private void setAppId(String appId) {
+ OAUtils.APPID = appId;
+ }
+ @Value("${pmhub.oa.code}")
+ private void setCode(String code) {
+ OAUtils.CODE = code;
+ }
+
+
+ @Value("${pmhub.oa.clientId}")
+ private void setClientId(String clientId) {
+ OAUtils.CLIENT_ID = clientId;
+ }
+
+ @Value("${pmhub.oa.clientSecret}")
+ private void setClientSecret(String clientSecret) {
+ OAUtils.CLIENT_SECRET = clientSecret;
+ }
+
+ /**
+ * 泛微地址
+ */
+ @Value("${pmhub.oa.path}")
+ private void setServerPath(String path) {
+ SERVER_PATH = path;
+ }
+
+ public static String getServerPath() {
+ return SERVER_PATH;
+ }
+
+ /**
+ * pmhub平台 host
+ * */
+ public static String host;
+ @Value("${pmhub.oa.host}")
+ private void setHost(String host) {
+ OAUtils.host = host;
+ }
+
+ /**
+ * pmhub平台 path2
+ * */
+ public static String path2;
+ @Value("${pmhub.oa.path2}")
+ private void setPath2(String path2) {
+ OAUtils.path2 = path2;
+ }
+
+ public static final String ssoPath = "/sso/oa?url=";
+
+ public static String ssoCreate(String target) {
+
+ String redirectUri = "";
+
+ try {
+ redirectUri = URLEncoder.encode(String.format(SERVER_PATH + OA_AUTHORIZE_URL, CLIENT_ID, URLEncoder.encode(host + path2 + ssoPath, UTF_8) + URLEncoder.encode(URLEncoder.encode(target, UTF_8), UTF_8)), UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ return redirectUri;
+
+ }
+
+ public static String ssoCreateLogin(String target, String target2) {
+
+ String redirectUri = "";
+
+ try {
+ redirectUri = URLEncoder.encode(target, UTF_8) + URLEncoder.encode(URLEncoder.encode(target2, UTF_8), UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ return redirectUri;
+
+ }
+
+ /**
+ * 获取access_token接口
+ */
+ public static String getOaAccessToken(String code, String redirectUri) {
+
+ JSONObject jsonObject = JSONUtil.parseObj(HttpRequest.post(String.format(SERVER_PATH + OA_ACCESS_TOKEN_URL, CLIENT_ID, CLIENT_SECRET, GRANT_TYPE, code, redirectUri))
+ .execute().body());
+
+ if (jsonObject.getInt(ERROR_CODE) != NUM_0) {
+ throw new ServiceException("调用泛微 OA 获取 access_token 接口失败");
+ }
+ return jsonObject.getStr(ACCESS_TOKEN);
+ }
+
+
+ /**
+ * 获取用户信息接口
+ */
+ public static String getOAUserInfo(String accessToken) {
+
+ // 获取用户信息
+ JSONObject jsonObject = JSONUtil.parseObj(HttpRequest.get(String.format(SERVER_PATH + OA_PROFILE_URL, accessToken))
+ .execute().body());
+ if (jsonObject.getInt(ERROR_CODE) != NUM_0) {
+ throw new ServiceException("调用泛微 OA 获取用户信息接口失败");
+ }
+ return jsonObject.getStr(USR_ID);
+ }
+
+
+ /**
+ * 认证退出接口
+ */
+ public static JSONObject logout(String service) {
+ // 获取用户基础信息
+ JSONObject ipJson = JSONUtil.parseObj(HttpRequest.get(String.format(SERVER_PATH + OA_LOGOUT_URL, service))
+ .execute().body());
+ return ipJson;
+ }
+
+ /**
+ * 第一步:
+ *
+ * 调用ecology注册接口,根据appid进行注册,将返回服务端公钥和Secret信息
+ */
+ public static void testRegist(String address) {
+
+ //获取当前系统RSA加密的公钥
+ RSA rsa = new RSA();
+ String publicKey = rsa.getPublicKeyBase64();
+ String privateKey = rsa.getPrivateKeyBase64();
+
+ // 客户端RSA私钥
+ SYSTEM_CACHE.put(LOCAL_PRIVATE_KEY, privateKey);
+ // 客户端RSA公钥
+ SYSTEM_CACHE.put(LOCAL_PUBLIC_KEY, publicKey);
+
+ //调用ECOLOGY系统接口进行注册
+ String data = HttpRequest.post(address + REGISTER_API)
+ .header(APP_ID, APPID)
+ .header(CPK, publicKey)
+ .timeout(TIME_2000)
+ .execute().body();
+
+ Map datas = JSONUtil.parseObj(data);
+
+ //ECOLOGY返回的系统公钥
+ SYSTEM_CACHE.put(SERVER_PUBLIC_KEY, StrUtil.nullToEmpty((String) datas.get(SPK)));
+ //ECOLOGY返回的系统密钥
+ SYSTEM_CACHE.put(SERVER_SECRET, StrUtil.nullToEmpty((String) datas.get(SECRIT)));
+ }
+ public static Map testRegist2(String address) {
+
+ //获取当前系统RSA加密的公钥
+ RSA rsa = new RSA();
+ String publicKey = rsa.getPublicKeyBase64();
+
+ //调用ECOLOGY系统接口进行注册
+ String data = HttpRequest.post(address + REGISTER_API)
+ .header(APP_ID, APPID)
+ .header(CPK, publicKey)
+ .timeout(TIME_2000)
+ .execute().body();
+
+ return JSONUtil.parseObj(data);
+ }
+
+ /**
+ * 第二步:
+ *
+ * 通过第一步中注册系统返回信息进行获取token信息
+ */
+ public static Map testGetoken(String address) {
+ // 从系统缓存或者数据库中获取ECOLOGY系统公钥和Secret信息
+ String secret = SYSTEM_CACHE.get(SERVER_SECRET);
+ String spk = SYSTEM_CACHE.get(SERVER_PUBLIC_KEY);
+
+ // 如果为空,说明还未进行注册,调用注册接口进行注册认证与数据更新
+ if (Objects.isNull(secret) || Objects.isNull(spk)) {
+ testRegist(address);
+ // 重新获取最新ECOLOGY系统公钥和Secret信息
+ secret = SYSTEM_CACHE.get(SERVER_SECRET);
+ spk = SYSTEM_CACHE.get(SERVER_PUBLIC_KEY);
+ }
+
+ // 公钥加密,所以RSA对象私钥为null
+ RSA rsa = new RSA(null, spk);
+ //对秘钥进行加密传输,防止篡改数据
+ String encryptSecret = rsa.encryptBase64(secret, CharsetUtil.CHARSET_UTF_8, KeyType.PublicKey);
+
+ //调用ECOLOGY系统接口进行注册
+ String data = HttpRequest.post(address + TOKEN_API)
+ .header(APP_ID, APPID)
+ .header(SECRET, encryptSecret)
+ .header(TIME_NAME, TIME_3600)
+ .execute().body();
+
+ Map datas = JSONUtil.parseObj(data);
+
+ //ECOLOGY返回的token
+ // TODO 为Token缓存设置过期时间
+ SYSTEM_CACHE.put(SERVER_TOKEN, StrUtil.nullToEmpty((String) datas.get(TOKEN)));
+
+ return datas;
+ }
+
+ public static String testGetToken(String address) {
+
+ Map map = testRegist2(address);
+
+ // 公钥加密,所以RSA对象私钥为null
+ RSA rsa = new RSA(null, StrUtil.nullToEmpty((String) map.get(SPK)));
+ //对秘钥进行加密传输,防止篡改数据
+ String encryptSecret = rsa.encryptBase64(StrUtil.nullToEmpty((String) map.get(SECRIT)), CharsetUtil.CHARSET_UTF_8, KeyType.PublicKey);
+
+ //调用ECOLOGY系统接口进行注册
+ String data = HttpRequest.post(address + TOKEN_API)
+ .header(APP_ID, APPID)
+ .header(SECRET, encryptSecret)
+ .header(TIME_NAME, TIME_3600)
+ .execute().body();
+
+ Map datas = JSONUtil.parseObj(data);
+
+ //ECOLOGY返回的token
+ return StrUtil.nullToEmpty((String) datas.get(TOKEN));
+ }
+ /**
+ * 第三步:
+ *
+ * 调用ecology系统的rest接口,请求头部带上token和用户标识认证信息
+ *
+ * @param api rest api 接口地址(该测试代码仅支持GET请求)
+ * @param jsonParams 请求参数json串
+ *
+ * 注意:ECOLOGY系统所有POST接口调用请求头请设置 "Content-Type","application/x-www-form-urlencoded; charset=utf-8"
+ */
+ public static JSONObject restfulCall(String api, String jsonParams, String type) {
+ log.info("调用泛微接口:{},{},{}", api, jsonParams, type);
+
+ // ECOLOGY 返回的 token
+ String token = SYSTEM_CACHE.get(SERVER_TOKEN);
+ if (StrUtil.isEmpty(token)) {
+ token = (String) testGetoken(SERVER_PATH).get(TOKEN);
+ }
+
+ String spk = SYSTEM_CACHE.get(SERVER_PUBLIC_KEY);
+ // 封装请求头参数
+ RSA rsa = new RSA(null, spk);
+
+ // 调用 ECOLOGY 系统接口
+ if (StringUtils.isNotBlank(type)) {
+ return JSONUtil.parseObj(HttpRequest.post(SERVER_PATH + api + jsonParams)
+ .header(APP_ID, APPID)
+ .header(TOKEN, token)
+ .header(SESSION, NUM_1)
+ .execute().body());
+ }
+ JSONObject jsonObject = JSONUtil.parseObj(HttpRequest.post(SERVER_PATH + api)
+ .header(APP_ID, APPID)
+ .header(TOKEN, token)
+ .header(SESSION, NUM_1)
+ .body(jsonParams)
+ .execute().body());
+ log.info("调用泛微查询人员信息返回:{}", jsonObject);
+ return jsonObject;
+ }
+
+ public static JSONObject restfulCall2(String api, String jsonParams, String type) {
+ log.info("调用泛微接口:{},{},{}", api, jsonParams, type);
+
+ // ECOLOGY 返回的 token
+ String token = testGetToken(SERVER_PATH);
+
+ // 调用 ECOLOGY 系统接口
+ if (StringUtils.isNotBlank(type)) {
+ return JSONUtil.parseObj(HttpRequest.post(SERVER_PATH + api + jsonParams)
+ .header(APP_ID, APPID)
+ .header(TOKEN, token)
+ .header(SESSION, NUM_1)
+ .execute().body());
+ }
+ JSONObject jsonObject = JSONUtil.parseObj(HttpRequest.post(SERVER_PATH + api)
+ .header(APP_ID, APPID)
+ .header(TOKEN, token)
+ .header(SESSION, NUM_1)
+ .body(jsonParams)
+ .execute().body());
+ log.info("调用泛微查询返回:{}", jsonObject);
+ return jsonObject;
+ }
+
+ /**
+ * 将 Map 转换成字符串参数,用于 POST GET 请求
+ *
+ * @param map
+ * @return
+ */
+ public static String mapToStr(Map map) {
+ StringBuilder stringBuilder = new StringBuilder();
+ if (map != null) {
+ for (Map.Entry entry : map.entrySet()) {
+ stringBuilder.append(entry.getKey());
+ if (entry.getValue() != null)
+ stringBuilder.append("=").append(entry.getValue());
+ stringBuilder.append("&");
+ }
+ }
+ if (stringBuilder.length() > 0)
+ return "?" + stringBuilder.substring(0, stringBuilder.length() - 1);
+ return null;
+ }
+
+ /**
+ * 发送消息,参数封装
+ * @return
+ */
+ public static Map sendCustomMessageSingle(String loginId, String title, String context, String redirectUri, String businessId) {
+
+ Map map = new HashMap<>();
+
+ // 消息来源,新建消息来源获取code 请查看文档第四大点补充
+ map.put("code", CODE);
+ //接收人登录名
+ map.put("loginIdList", loginId);
+ //creater的值 创建人OA系统id / 创建人登录名 / 创建人编号 / 创建人姓名(对应接收人所传入的形式)
+ map.put("creater", "1");
+ map.put("title", title);
+ map.put("context", context);
+ map.put("linkUrl", redirectUri);
+ map.put("linkMobileUrl", redirectUri);
+ //消息来源code +“|”+业务id 消息需要打上已处理标记
+ if (StringUtils.isNotBlank(businessId)) {
+ map.put("targetId", CODE + "|" + businessId);
+ }
+
+ return map;
+ }
+
+ /**
+ * 将消息打上已处理标记,参数封装
+ * @return
+ */
+ public static Map alterCustomMessageSingle(String businessId, String state, String loginId) {
+
+ Map map = new HashMap<>();
+ //修改消息状态时所依据的条件,在消息发送时也需要插入,字符串拼接方式为业务id前加消息来源的“code|"
+ map.put("targetId", CODE + "|" + businessId);
+ //待处理 0 已处理 1 已同意 2 已拒绝 3 已删除 27 已暂停 34 已撤销 35
+ map.put("bizState", state);
+ map.put("loginIdList", loginId);
+
+ return map;
+ }
+
+
+ /**
+ * 删除消息,参数封装
+ * @return
+ */
+ public static Map deleteCustomMessageSingle(Long userId, String state, String loginId) {
+
+ Map map = new HashMap<>();
+ //修改消息状态时所依据的条件,在消息发送时也需要插入,字符串拼接方式为业务id前加消息来源的“code|"
+ map.put("targetId", CODE + "|" + userId);
+ //待处理 0 已处理 1 已同意 2 已拒绝 3 已删除 27 已暂停 34 已撤销 35
+ map.put("bizState", state);
+ map.put("loginIdList", loginId);
+
+ return map;
+ }
+
+
+}
+
+
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PKCS7Encoder.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PKCS7Encoder.java
new file mode 100644
index 00000000..a5e4b90a
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PKCS7Encoder.java
@@ -0,0 +1,67 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解密接口.
+ */
+class PKCS7Encoder {
+ static Charset CHARSET = Charset.forName("utf-8");
+ static int BLOCK_SIZE = 32;
+
+ /**
+ * 获得对明文进行补位填充的字节.
+ *
+ * @param count 需要进行填充补位操作的明文字节个数
+ * @return 补齐用的字节数组
+ */
+ static byte[] encode(int count) {
+ // 计算需要填充的位数
+ int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+ if (amountToPad == 0) {
+ amountToPad = BLOCK_SIZE;
+ }
+ // 获得补位所用的字符
+ char padChr = chr(amountToPad);
+ String tmp = new String();
+ for (int index = 0; index < amountToPad; index++) {
+ tmp += padChr;
+ }
+ return tmp.getBytes(CHARSET);
+ }
+
+ /**
+ * 删除解密后明文的补位字符
+ *
+ * @param decrypted 解密后的明文
+ * @return 删除补位字符后的明文
+ */
+ static byte[] decode(byte[] decrypted) {
+ int pad = (int) decrypted[decrypted.length - 1];
+ if (pad < 1 || pad > 32) {
+ pad = 0;
+ }
+ return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+ }
+
+ /**
+ * 将数字转化成ASCII码对应的字符,用于对明文进行补码
+ *
+ * @param a 需要转化的数字
+ * @return 转化得到的字符
+ */
+ static char chr(int a) {
+ byte target = (byte) (a & 0xFF);
+ return (char) target;
+ }
+
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PublicUtil.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PublicUtil.java
new file mode 100644
index 00000000..1279aa9d
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/PublicUtil.java
@@ -0,0 +1,243 @@
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import cn.hutool.cache.CacheUtil;
+import cn.hutool.cache.impl.TimedCache;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.log.LogFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * 企业微信工具类
+ * @author canghe
+ */
+@Component
+public class PublicUtil {
+
+
+ /**
+ * 单点登录用户信息验证缓存,证明是由企业微信过来的用户,限制浏览器直接请求(超时时间为五分钟)
+ * */
+ private static TimedCache wxUserCache = CacheUtil.newTimedCache(5*60*1000);
+
+ /**
+ * 企微鉴权接口地址
+ * */
+ private static final String WORK_WX_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
+ /**
+ * 企微IP列表接口地址
+ * */
+ private static final String WORK_WX_IP_URL = " https://qyapi.weixin.qq.com/cgi-bin/get_api_domain_ip?access_token=%s";
+
+ /**
+ * 企微用户基础信息查询接口地址
+ * */
+ private static final String WORK_WX_USER_BASE_INFO = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s";
+
+ /**
+ * 企微用户公开信息查询接口地址
+ * */
+ private static final String WORK_WX_USER_PUBLIC_INFO = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s";
+
+ /**
+ * 企微用户敏感信息查询接口地址
+ * */
+ private static final String WORK_WX_USER_DETAIL_INFO = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s";
+
+ /**
+ * 错误代码的key
+ * */
+ public static final String ERR_CODE_KEY_NAME = "errcode";
+
+ /**
+ * 错误信息的key
+ * */
+ public static final String ERR_MSG_KEY_NAME = "errmsg";
+
+ /**
+ * userid的key
+ * */
+ public static final String USR_ID_KEY = "userid";
+
+ /**
+ * 证明是企微单点登录的用户token
+ * */
+ public static final String USR_TOKEN_KEY = "tmp_token";
+
+ /**
+ * 敏感信息ticket的key
+ * */
+ public static final String USR_TICKET_KEY = "user_ticket";
+
+ /**
+ * 用户姓名的key
+ * */
+ public static final String USR_NICK_KEY = "name";
+
+ /**
+ * 用户手机的key
+ * */
+ public static final String USR_MOBILE_KEY = "mobile";
+
+ /**
+ * 用户邮箱的key
+ * */
+ public static final String USR_EMAIL_KEY = "biz_mail";
+
+ /**
+ * 用户头像的key
+ * */
+ public static final String USR_AVATAR_KEY = "avatar";
+
+
+ /**
+ * 获取到的凭证,最长为512字节
+ */
+ private static String ACCESS_TOKEN = "ACCESS_TOKEN";
+
+ /**
+ * 企业ID
+ * */
+ private static String CORP_ID = "CORP_ID";
+
+ /**
+ * 应用的凭证密钥
+ * */
+ private static String CORP_SECRET = "CORP_SECRET";
+
+ /**
+ * 应用id
+ * */
+ private static Integer AGENT_ID = 0;
+
+ /**
+ * 后端代理转发路由
+ * */
+ private static String SERVER_PATH = "";
+
+
+ @Value("${pmhub.workWx.corpid}")
+ private void setCorpId(String corpId) {
+ PublicUtil.CORP_ID = corpId;
+ }
+
+ @Value("${pmhub.workWx.corpsecret}")
+ private void setCorpSecret(String corpSecret) {
+ PublicUtil.CORP_SECRET = corpSecret;
+ }
+
+ @Value("${pmhub.workWx.agentid}")
+ private void setAgentId(Integer agentid) {
+ PublicUtil.AGENT_ID = agentid;
+ }
+
+ public static Integer getAgentId(){
+ return AGENT_ID;
+ }
+
+ @Value("${pmhub.workWx.path}")
+ private void setServerPath(String path) {
+ SERVER_PATH = path;
+ }
+
+ public static String getServerPath() {
+ return SERVER_PATH;
+ }
+
+ /**
+ * 获取企微token
+ * */
+ public static String getWorkWxToken(){
+
+ // 拉取企微ip地址列表,测试token是否还有效,无效则重新获取企微token
+ JSONObject ipJson = JSONUtil.parseObj(HttpRequest.get(String.format(WORK_WX_IP_URL,ACCESS_TOKEN))
+ .execute().body());
+
+ if (ipJson.getInt(ERR_CODE_KEY_NAME) != 0){
+ String result2 = HttpRequest.get(String.format(WORK_WX_URL,CORP_ID,CORP_SECRET))
+ .execute().body();
+ JSONObject resultJson = JSONUtil.parseObj(result2);
+ try {
+ if (resultJson.getInt(ERR_CODE_KEY_NAME) == 0){
+ ACCESS_TOKEN = resultJson.getStr("access_token");
+ }else {
+ throw new RuntimeException(resultJson.getStr(ERR_MSG_KEY_NAME));
+ }
+ }catch (Exception ex){
+ LogFactory.get().error(ex);
+ throw new RuntimeException(ex.getMessage());
+ }
+ }
+ return ACCESS_TOKEN;
+ }
+
+
+ /**
+ * 通过用户code获取用户基础信息
+ * */
+ public static JSONObject getWxUserBaseInfo(String code){
+ // 获取用户基础信息
+ JSONObject ipJson = JSONUtil.parseObj(HttpRequest.get(String.format(WORK_WX_USER_BASE_INFO,getWorkWxToken(),code))
+ .execute().body());
+
+ if (ipJson.getInt(ERR_CODE_KEY_NAME) == 0){
+ String token = IdUtil.randomUUID();
+ wxUserCache.put(ipJson.getStr(USR_ID_KEY), token);
+ ipJson.set(USR_TOKEN_KEY,token);
+ }
+
+ return ipJson;
+ }
+
+ /**
+ * 比对token 判断是否是企微过来的用户
+ * @return 是否是企微SSO请求
+ * */
+ public static Boolean checkToken(String userName,String token){
+ if (StringUtils.isEmpty(userName)||StringUtils.isEmpty(token)){
+ return false;
+ }else {
+ String uToken = wxUserCache.get(userName);
+ if (!StringUtils.isEmpty(uToken)){
+ if (token.equals(uToken)){
+ // 验证成功,移除token防止被重复使用
+ wxUserCache.remove(userName);
+ return true;
+ }else {
+ return false;
+ }
+ }else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 通过用户名获取用户姓名等公开信息
+ * */
+ public static JSONObject getWxUserPublicInfo(String userName){
+ // 获取用户基础信息
+ JSONObject ipJson = JSONUtil.parseObj(HttpRequest.get(String.format(WORK_WX_USER_PUBLIC_INFO,getWorkWxToken(),userName))
+ .execute().body());
+ return ipJson;
+ }
+
+
+ /**
+ * 通过用户ticket获取用户敏感信息
+ * */
+ public static JSONObject getWxUserDetailInfo(String ticket){
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.set("user_ticket",ticket);
+ // 获取用户敏感信息
+ JSONObject userInfo = JSONUtil.parseObj(HttpRequest.post(String.format(WORK_WX_USER_DETAIL_INFO,getWorkWxToken()))
+ .body(jsonObject.toString())
+ .execute().body());
+ return userInfo;
+ }
+
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RedisUtils.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RedisUtils.java
new file mode 100644
index 00000000..586a1812
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RedisUtils.java
@@ -0,0 +1,91 @@
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import com.laigeoffer.pmhub.base.core.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+/**
+ * @author canghe
+ * @date 2023-04-10 11:04
+ */
+@Slf4j
+@Component
+public class RedisUtils {
+
+ @Resource
+ private RedisTemplate redisTemplate;
+
+ /**
+ * 赋值一个静态的redis
+ */
+ public static RedisTemplate redis;
+
+ /**
+ * 此注解表示构造时赋值
+ */
+ @PostConstruct
+ public void redisTemplate() {
+ redis = this.redisTemplate;
+ }
+
+
+ /**
+ * 根据key读取数据
+ */
+ public static Object get(final String key) {
+ if (StringUtils.isBlank(key)) {
+ return null;
+ }
+ try {
+ return redis.opsForValue().get(key);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * 写入数据
+ */
+ public static boolean set(final String key, Object value) {
+ if (StringUtils.isBlank(key)) {
+ return false;
+ }
+ try {
+ redis.opsForValue().set(key, value);
+ log.info("存入redis成功,key:{},value:{}", key, value);
+ return true;
+ } catch (Exception e) {
+ log.error("存入redis失败,key:{},value:{}", key, value);
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ /**
+ * 删除对应的value
+ *
+ * @param key
+ */
+
+ public static void remove(final String key) {
+ if (exists(key)) {
+ redis.delete(key);
+ }
+ }
+
+ /**
+ * 判断缓存中是否有对应的value
+ *
+ * @param key
+ * @return
+ */
+
+ public static boolean exists(final String key) {
+ return Boolean.TRUE.equals(redis.hasKey(key));
+ }
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RocketMqUtils.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RocketMqUtils.java
new file mode 100644
index 00000000..678022fa
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/RocketMqUtils.java
@@ -0,0 +1,127 @@
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.log.LogFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.laigeoffer.pmhub.base.notice.domain.dto.ProcessWxMessageStateUpdateDTO;
+import com.laigeoffer.pmhub.base.notice.domain.entity.WxResult;
+import com.laigeoffer.pmhub.base.notice.enums.ButtonStateEnum;
+import org.apache.rocketmq.client.apis.ClientConfiguration;
+import org.apache.rocketmq.client.apis.ClientConfigurationBuilder;
+import org.apache.rocketmq.client.apis.ClientException;
+import org.apache.rocketmq.client.apis.ClientServiceProvider;
+import org.apache.rocketmq.client.apis.message.Message;
+import org.apache.rocketmq.client.apis.producer.Producer;
+import org.apache.rocketmq.client.apis.producer.SendReceipt;
+
+import java.io.IOException;
+
+/**
+ * RocketMQ连接工具
+ * @author canghe
+ */
+//@Component
+public class RocketMqUtils {
+
+
+ /**
+ * 连接地址
+ * */
+ private static String addr;
+
+
+ /**
+ * 微信topic
+ * */
+ private static String WX_TOPIC;
+
+// @Value("${pmhub.rocketMQ.addr}")
+ private void setAddr(String addr) {
+ RocketMqUtils.addr = addr;
+ }
+// @Value("${pmhub.rocketMQ.topic.wxMessage}")
+ public void setWxTopic(String wxMessage) {
+ WX_TOPIC = wxMessage;
+ }
+
+ /**
+ * rocketmq 消息Tag
+ * */
+ public final static String mqTag = "WX_MASSAGE";
+
+
+
+
+
+
+
+
+
+ /**
+ * 推送到微信topic
+ * */
+ public static void push2Wx(com.laigeoffer.pmhub.base.notice.domain.entity.Message ob){
+
+ try {
+
+ String key = IdUtil.simpleUUID();
+ ObjectMapper objectMapper = new ObjectMapper();
+ // 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
+ String endpoint = addr;
+ // 消息发送的目标Topic名称,需要提前创建。
+ String topic = WX_TOPIC;
+ ClientServiceProvider provider = ClientServiceProvider.loadService();
+ ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint);
+ ClientConfiguration configuration = builder.build();
+ // 初始化Producer时需要设置通信配置以及预绑定的Topic。
+ Producer producer = provider.newProducerBuilder()
+ .setTopics(topic)
+ .setClientConfiguration(configuration)
+ .build();
+ // 普通消息发送。
+ Message message = provider.newMessageBuilder()
+ .setTopic(topic)
+ // 设置消息索引键,可根据关键字精确查找某条消息。
+ .setKeys(key)
+ // 设置消息Tag,用于消费端根据指定Tag过滤消息。
+ .setTag(mqTag)
+ // 消息体。
+ .setBody(objectMapper.writeValueAsString(ob).getBytes())
+ .build();
+
+ // 发送消息,需要关注发送结果,并捕获失败等异常。
+ SendReceipt sendReceipt = producer.send(message);
+ LogFactory.get().info("Send message successfully, messageId={}", sendReceipt.getMessageId());
+
+ producer.close();
+ } catch (ClientException | IOException e) {
+ LogFactory.get().error("推送微信消息时发生错误:", e);
+ }
+
+ }
+
+
+ public static void cleanMessage(String instId){
+ LogFactory.get().info("清理消息:" + instId);
+ try {
+ // 清理消息
+ MessageDataDTO messageDataDTO = (MessageDataDTO) RedisUtils.get(instId);
+ if (messageDataDTO != null) {
+ ProcessWxMessageStateUpdateDTO processWxMessageStateUpdateDTO = new ProcessWxMessageStateUpdateDTO();
+ processWxMessageStateUpdateDTO.setAtall(1);
+ processWxMessageStateUpdateDTO.setResponse_code(messageDataDTO.getMsgCode());
+ processWxMessageStateUpdateDTO.getButton().setReplace_name(ButtonStateEnum.FINISH);
+ WxResult wxResult = MessageUtils.updateMessage(processWxMessageStateUpdateDTO);
+ LogFactory.get().info(JSONUtil.toJsonStr(wxResult));
+ RedisUtils.remove(instId);
+ LogFactory.get().info("消息存在");
+ }
+ } catch (Exception ex){
+ LogFactory.get().info("消息不存在");
+ }
+ }
+
+
+
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SHA1.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SHA1.java
new file mode 100644
index 00000000..9013e4c4
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SHA1.java
@@ -0,0 +1,64 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.laigeoffer.pmhub.base.notice.utils;
+
+
+import com.laigeoffer.pmhub.base.notice.exception.AesException;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * SHA1 class
+ *
+ * 计算消息签名接口.
+ */
+class SHA1 {
+
+ /**
+ * 用SHA1算法生成安全签名
+ * @param token 票据
+ * @param timestamp 时间戳
+ * @param nonce 随机字符串
+ * @param encrypt 密文
+ * @return 安全签名
+ * @throws AesException
+ */
+ public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
+ {
+ try {
+ String[] array = new String[] { token, timestamp, nonce, encrypt };
+ StringBuffer sb = new StringBuffer();
+ // 字符串排序
+ Arrays.sort(array);
+ for (int i = 0; i < 4; i++) {
+ sb.append(array[i]);
+ }
+ String str = sb.toString();
+ // SHA1签名生成
+ 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) {
+ e.printStackTrace();
+ throw new AesException(AesException.ComputeSignatureError);
+ }
+ }
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SsoUrlUtils.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SsoUrlUtils.java
new file mode 100644
index 00000000..659df229
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/SsoUrlUtils.java
@@ -0,0 +1,23 @@
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import cn.hutool.core.util.URLUtil;
+
+/**
+ * 企微单点登录地址创建
+ * @author canghe
+ */
+public class SsoUrlUtils {
+
+ public static final String ssoPath = "/sso/wx?url=";
+
+ public static final String wxOauth2Url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=STATE&agentid=%s#wechat_redirect";
+
+
+ /**
+ *
+ * */
+ public static String ssoCreate(String appid,String agentid,String taget){
+ return String.format(wxOauth2Url,appid, URLUtil.encode(taget),agentid);
+ }
+
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/StringCreateUtils.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/StringCreateUtils.java
new file mode 100644
index 00000000..04140b39
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/StringCreateUtils.java
@@ -0,0 +1,29 @@
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import java.util.List;
+
+/**
+ * 字符串拼接工具
+ * @author canghe
+ */
+public class StringCreateUtils {
+
+ /**
+ * 将文本数组合并为一个用分隔符拼接的字符串
+ * @param list 文本数组
+ * @param flag 分隔符
+ * @return 拼接的字符串
+ * */
+ public static String listStringCompose(List list,String flag){
+ StringBuilder str = new StringBuilder();
+ for (int i=0;i
+ * 第三方回复加密消息给企业微信
+ * 第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。
+ *
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
+ *
+ * - 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
+ * ...
+ * - 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
+ * - 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
+ * - 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
+ *
+ * @author canghe
+ */
+public class WXBizMsgCrypt {
+ static Charset CHARSET = Charset.forName("utf-8");
+ Base64 base64 = new Base64();
+ byte[] aesKey;
+ String token;
+ String receiveid;
+
+ /**
+ * 构造函数
+ * @param token 企业微信后台,开发者设置的token
+ * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
+ * @param receiveid, 不同场景含义不同,详见文档
+ *
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+ */
+ public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
+ if (encodingAesKey.length() != 43) {
+ throw new AesException(AesException.IllegalAesKey);
+ }
+
+ this.token = token;
+ this.receiveid = receiveid;
+ aesKey = Base64.decode(encodingAesKey + "=");
+ }
+
+ // 生成4个字节的网络字节序
+ byte[] getNetworkBytesOrder(int sourceNumber) {
+ byte[] orderBytes = new byte[4];
+ orderBytes[3] = (byte) (sourceNumber & 0xFF);
+ orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
+ orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
+ orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
+ return orderBytes;
+ }
+
+ // 还原4个字节的网络字节序
+ int recoverNetworkBytesOrder(byte[] orderBytes) {
+ int sourceNumber = 0;
+ for (int i = 0; i < 4; i++) {
+ sourceNumber <<= 8;
+ sourceNumber |= orderBytes[i] & 0xff;
+ }
+ return sourceNumber;
+ }
+
+ // 随机生成16位字符串
+ String getRandomStr() {
+ String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ Random random = new Random();
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < 16; i++) {
+ int number = random.nextInt(base.length());
+ sb.append(base.charAt(number));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 对明文进行加密.
+ *
+ * @param text 需要加密的明文
+ * @return 加密后base64编码的字符串
+ * @throws AesException aes加密失败
+ */
+ String encrypt(String randomStr, String text) throws AesException {
+ ByteGroup byteCollector = new ByteGroup();
+ byte[] randomStrBytes = randomStr.getBytes(CHARSET);
+ byte[] textBytes = text.getBytes(CHARSET);
+ byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
+ byte[] receiveidBytes = receiveid.getBytes(CHARSET);
+
+ // randomStr + networkBytesOrder + text + receiveid
+ byteCollector.addBytes(randomStrBytes);
+ byteCollector.addBytes(networkBytesOrder);
+ byteCollector.addBytes(textBytes);
+ byteCollector.addBytes(receiveidBytes);
+
+ // ... + pad: 使用自定义的填充方式对明文进行补位填充
+ byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
+ byteCollector.addBytes(padBytes);
+
+ // 获得最终的字节流, 未加密
+ byte[] unencrypted = byteCollector.toBytes();
+
+ try {
+ // 设置加密模式为AES的CBC模式
+ 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);
+
+ // 使用BASE64对加密后的字符串进行编码
+ String base64Encrypted = Base64.encode(encrypted);
+
+ return base64Encrypted;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AesException(AesException.EncryptAESError);
+ }
+ }
+
+ /**
+ * 对密文进行解密.
+ *
+ * @param text 需要解密的密文
+ * @return 解密得到的明文
+ * @throws AesException aes解密失败
+ */
+ String decrypt(String text) throws AesException {
+ byte[] original;
+ try {
+ // 设置解密模式为AES的CBC模式
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
+ IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
+ cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+
+ // 使用BASE64对密文进行解码
+ byte[] encrypted = Base64.decode(text);
+
+ // 解密
+ original = cipher.doFinal(encrypted);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AesException(AesException.DecryptAESError);
+ }
+
+ String xmlContent, from_receiveid;
+ try {
+ // 去除补位字符
+ byte[] bytes = PKCS7Encoder.decode(original);
+
+ // 分离16位随机字符串,网络字节序和receiveid
+ byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+ int xmlLength = recoverNetworkBytesOrder(networkOrder);
+
+ xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
+ from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
+ CHARSET);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AesException(AesException.IllegalBuffer);
+ }
+
+ // receiveid不相同的情况
+ if (!from_receiveid.equals(receiveid)) {
+ throw new AesException(AesException.ValidateCorpidError);
+ }
+ return xmlContent;
+
+ }
+
+ /**
+ * 将企业微信回复用户的消息加密打包.
+ *
+ * - 对要发送的消息进行AES-CBC加密
+ * - 生成安全签名
+ * - 将消息密文和安全签名打包成xml格式
+ *
+ *
+ * @param replyMsg 企业微信待回复用户的消息,xml格式的字符串
+ * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
+ * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
+ *
+ * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+ */
+ public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
+ // 加密
+ String encrypt = encrypt(getRandomStr(), replyMsg);
+
+ // 生成安全签名
+ if (timeStamp == "") {
+ timeStamp = Long.toString(System.currentTimeMillis());
+ }
+
+ String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
+
+ // System.out.println("发送给平台的签名是: " + signature[1].toString());
+ // 生成发送的xml
+ String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
+ return result;
+ }
+
+ /**
+ * 检验消息的真实性,并且获取解密后的明文.
+ *
+ * - 利用收到的密文生成安全签名,进行签名验证
+ * - 若验证通过,则提取xml中的加密消息
+ * - 对消息进行解密
+ *
+ *
+ * @param msgSignature 签名串,对应URL参数的msg_signature
+ * @param timeStamp 时间戳,对应URL参数的timestamp
+ * @param nonce 随机串,对应URL参数的nonce
+ * @param postData 密文,对应POST请求的数据
+ *
+ * @return 解密后的原文
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+ */
+ public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
+ throws AesException {
+
+ // 密钥,公众账号的app secret
+ // 提取密文
+ Object[] encrypt = XMLParse.extract(postData);
+
+ // 验证安全签名
+ String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
+
+ // 和URL中的签名比较是否相等
+ // System.out.println("第三方收到URL中的签名:" + msg_sign);
+ // System.out.println("第三方校验签名:" + signature);
+ if (!signature.equals(msgSignature)) {
+ throw new AesException(AesException.ValidateSignatureError);
+ }
+
+ // 解密
+ String result = decrypt(encrypt[1].toString());
+ return result;
+ }
+
+ /**
+ * 验证URL
+ * @param msgSignature 签名串,对应URL参数的msg_signature
+ * @param timeStamp 时间戳,对应URL参数的timestamp
+ * @param nonce 随机串,对应URL参数的nonce
+ * @param echoStr 随机串,对应URL参数的echostr
+ *
+ * @return 解密之后的echostr
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+ */
+ public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
+ throws AesException {
+ String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
+
+ if (!signature.equals(msgSignature)) {
+ throw new AesException(AesException.ValidateSignatureError);
+ }
+
+ String result = decrypt(echoStr);
+ return result;
+ }
+
+}
\ No newline at end of file
diff --git a/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/XMLParse.java b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/XMLParse.java
new file mode 100644
index 00000000..0de79092
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/java/com/laigeoffer/pmhub/base/notice/utils/XMLParse.java
@@ -0,0 +1,104 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.laigeoffer.pmhub.base.notice.utils;
+
+import com.laigeoffer.pmhub.base.notice.exception.AesException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.StringReader;
+
+/**
+ * XMLParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+class XMLParse {
+
+ /**
+ * 提取出xml数据包中的加密消息
+ * @param xmltext 待提取的xml字符串
+ * @return 提取出的加密消息字符串
+ * @throws AesException
+ */
+ public static Object[] extract(String xmltext) throws AesException {
+ Object[] result = new Object[3];
+ try {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+
+ String FEATURE = null;
+ // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
+ // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
+ FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
+ dbf.setFeature(FEATURE, true);
+
+ // If you can't completely disable DTDs, then at least do the following:
+ // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+ // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+ // JDK7+ - http://xml.org/sax/features/external-general-entities
+ FEATURE = "http://xml.org/sax/features/external-general-entities";
+ dbf.setFeature(FEATURE, false);
+
+ // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
+ // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
+ // JDK7+ - http://xml.org/sax/features/external-parameter-entities
+ FEATURE = "http://xml.org/sax/features/external-parameter-entities";
+ dbf.setFeature(FEATURE, false);
+
+ // Disable external DTDs as well
+ FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+ dbf.setFeature(FEATURE, false);
+
+ // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
+ dbf.setXIncludeAware(false);
+ dbf.setExpandEntityReferences(false);
+
+ // And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then
+ // ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
+ // (http://cwe.mitre.org/data/definitions/918.html) and denial
+ // of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."
+
+ // remaining parser logic
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ StringReader sr = new StringReader(xmltext);
+ InputSource is = new InputSource(sr);
+ Document document = db.parse(is);
+
+ Element root = document.getDocumentElement();
+ NodeList nodelist1 = root.getElementsByTagName("Encrypt");
+ result[0] = 0;
+ result[1] = nodelist1.item(0).getTextContent();
+ return result;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AesException(AesException.ParseXmlError);
+ }
+ }
+
+ /**
+ * 生成xml消息
+ * @param encrypt 加密后的消息密文
+ * @param signature 安全签名
+ * @param timestamp 时间戳
+ * @param nonce 随机字符串
+ * @return 生成的xml字符串
+ */
+ public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+ String format = "\n" + "\n"
+ + "\n"
+ + "%3$s\n" + "\n" + "";
+ return String.format(format, encrypt, signature, timestamp, nonce);
+
+ }
+}
diff --git a/pmhub-base/pmhub-base-notice/src/main/resources/META-INF/spring.factories b/pmhub-base/pmhub-base-notice/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..bda1a280
--- /dev/null
+++ b/pmhub-base/pmhub-base-notice/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.laigeoffer.pmhub.base.datasource.properties.DruidProperties,\
+com.laigeoffer.pmhub.base.datasource.config.DruidConfig
diff --git a/pmhub-base/pom.xml b/pmhub-base/pom.xml
index d2763317..bdad7135 100644
--- a/pmhub-base/pom.xml
+++ b/pmhub-base/pom.xml
@@ -24,6 +24,7 @@
pmhub-base-sensitive
pmhub-base-swagger
pmhub-base-datasource
+ pmhub-base-notice
diff --git a/pom.xml b/pom.xml
index b6033843..d5480acc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
0.0.1
pmhub
- 🔥热门推荐🔥大学春招、秋招、应届项目,SpringCloud+JDK8+RocketMQ等技术架构,完成项目管理+BPM流程管理+AI智能项目,帮助学生主打就业的项目。
+ 🔥热门推荐🔥大学春招、秋招、应届项目,SpringCloud + SpringCloud Alibaba + RocketMQ等技术架构,完成项目管理+BPM流程管理+AI智能项目,帮助学生主打就业的项目。
pmhub-gateway
@@ -348,8 +348,6 @@
-
-