监控告警 http 代理企业微信服务

本贴最后更新于 2547 天前,其中的信息可能已经渤澥桑田

1. 项目背景

朋友在一家日企,运维工具老旧,希望增加一款系统告警工具。我提议通过微信企业号(改版为企业微信)或者短信推送告警信息。提出两点要求:

  1. 可以群发给企业微信中的多个人
  2. 监测健康检测服务是否存活(绕嘴。。)

做了第一版 demo,定位为通信渠道的 http 代理。贴出企业微信相关代码,如果有需要的同学可以拿去用,记得点个 star 就好。
https://github.com/wangyuheng/pharos

1.1 依赖项目

因为定位为 http 代理,并未使用数据库及持久化工具。

  1. springboot
  2. 企业微信接口 https://work.weixin.qq.com/api/doc
  3. 可能是目前最好最全的微信 Java 开发工具包(SDK)https://github.com/Wechat-Group/weixin-java-tools
  4. swagger 工具 https://github.com/wangyuheng/spring-boot-swagger-starter

2. 项目 code

代理入口为健康检测工具,出口为企业微信、短信等。同时需要保障和健康检测工具之间的网络通畅。

_105ce1b4cc3a436bb524ccb89cfc4d2cpng

最近在尝试画图

2.1 微信接口

企业微信相关配置如下

wechat: cp: corpid: agentid: corp: secret:
  1. corpid 企业 ID
  2. agentid 应用 ID,在企业微信管理后台创建应用后,可以查看应用 ID
  3. corp.secret 应用的凭证密钥

weixin-java-tools 已经对微信接口进行了友好的封装,可以通过标签、部门等分组,查询用户标识。配置 bean 方法如下

package com.crick.business.pharos.config; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.impl.WxCpDepartmentServiceImpl; import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; import me.chanjar.weixin.cp.api.impl.WxCpTagServiceImpl; import me.chanjar.weixin.cp.api.impl.WxCpUserServiceImpl; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import me.chanjar.weixin.cp.config.WxCpInMemoryConfigStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass(WxCpService.class) public class WechatCpConfig { Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${wechat.cp.corpid}") private String corpid; @Value("${wechat.cp.corp.secret}") private String corpSecret; @Value("${wechat.cp.agentid}") private Integer agentid; @Bean @ConditionalOnMissingBean public WxCpConfigStorage configStorage() { WxCpInMemoryConfigStorage configStorage = new WxCpInMemoryConfigStorage(); logger.info("****************wechat properties start****************"); logger.info("corpid:{}", corpid); logger.info("corpSecret:{}", corpSecret); logger.info("agentid:{}", agentid); logger.info("****************wechat properties end****************"); configStorage.setCorpId(corpid); configStorage.setCorpSecret(corpSecret); configStorage.setAgentId(agentid); return configStorage; } @Bean @ConditionalOnMissingBean public WxCpService WxCpService(WxCpConfigStorage configStorage) { WxCpService wxCpService = new WxCpServiceImpl(); wxCpService.setWxCpConfigStorage(configStorage); wxCpService.setTagService(new WxCpTagServiceImpl(wxCpService)); wxCpService.setDepartmentService(new WxCpDepartmentServiceImpl(wxCpService)); wxCpService.setUserService(new WxCpUserServiceImpl(wxCpService)); return wxCpService; } }

同时也提供了发送消息、已经封装消息的类

package com.crick.business.pharos.service; import me.chanjar.weixin.cp.bean.WxCpMessage; public class AlertTextBuilder { private Integer agentid; public AlertTextBuilder(Integer agentid) { this.agentid = agentid; } public WxCpMessage buildForTag(String content, String tag) { return WxCpMessage.TEXT().agentId(agentid).content(content).toTag(tag).build(); } public WxCpMessage buildForUsers(String content, String users) { return WxCpMessage.TEXT().agentId(agentid).content(content).toUser(users).build(); } }
package com.crick.business.pharos.service; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class WechatAlertService implements AlertService { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private WxCpService wxCpService; @Autowired private AlertTextBuilder alertTextBuilder; @Override public void alertTextToTag(String content, String tag) { try { wxCpService.messageSend(alertTextBuilder.buildForTag(content, tag)); } catch (WxErrorException e) { logger.error("alertTextToTag error! tag:{}", tag, e); } } @Override public void alertTextToUsers(String content, List<String> users) { try { wxCpService.messageSend(alertTextBuilder.buildForUsers(content, String.join(",", users))); } catch (WxErrorException e) { logger.error("alertTextToUsers error! users:{}", users, e); } } @Override public void alertTextToDepartment(String content, Integer department) { try { List<WxCpUser> wxCpUserList = wxCpService.getUserService().listSimpleByDepartment(department, true, 0); if (null != wxCpUserList) { String userList = wxCpUserList.stream() .map(WxCpUser::getUserId) .collect(Collectors.joining(",")); wxCpService.messageSend(alertTextBuilder.buildForUsers(content, userList)); } } catch (WxErrorException e) { logger.error("alertTextToDepartment error! department:{}", department, e); } } }

2.2 restful 接口

对外暴露的接口主要提供两个功能

  1. 代理企业微信,用于查询部门、标签、userId 等
  2. 发送告警信息

具体实现在 service 中,restful 暴露了 http 请求接口及 swagger 接口。并且将首页指向了 swagger 页面

@Controller public class IndexController { @GetMapping("/") public String index() { return "redirect:swagger-ui.html"; } }

_182d611babe14fe2b1f1b3b835079623png

2.3 验权

两种验权方式

  1. 参数 +secret 通过 SHA 加密签名
  2. ip 白名单

通过 interceptor 实现

2.3.1 白名单校验

public class AuthorInterceptor extends HandlerInterceptorAdapter { @Value("#{'${white.list}'.split(',')}") private List<String> whiteList; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.getBeanType().isAnnotationPresent(Anonymous.class)) { return true; } } String clientIp = getIpAddress(request); if (!whiteList.contains(clientIp)) { throw new RestfulException("client ip: " + clientIp + " not in white list", RestfulErrorCode.AUTHOR_ERROR); } return super.preHandle(request, response, handler); } private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }

2.3.2 接口参数签名校验

public class SignInterceptor extends HandlerInterceptorAdapter { @Value("${secret.key}") public String secretKey; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.getBeanType().isAnnotationPresent(Anonymous.class)) { return true; } } String sign = request.getParameter("sign"); if (null == sign || "".equals(sign)) { throw new RestfulException("must have a sign param!", RestfulErrorCode.SIGN_ERROR); } else { Map<String, String[]> parameters = request.getParameterMap(); if (parameters.size() > 0) { StringBuilder sb = new StringBuilder(); for (String key : parameters.keySet()) { if ("sign".equals(key)) { continue; } sb.append(key).append("-").append(Arrays.toString(parameters.get(key))).append("-"); } sb.append("token").append("-").append(secretKey); if (!sign.equals(EncryptUtil.sha1(sb.toString()))) { throw new RestfulException("sign check fail!", RestfulErrorCode.SIGN_ERROR); } } } return super.preHandle(request, response, handler); } }

SHA 加密工具封装

public class EncryptUtil { private EncryptUtil() { } private static final String SHA_1_ALGORITHM = "SHA-1"; private static final String SHA_256_ALGORITHM = "SHA-256"; public static String sha1(String source) { return sha(source, SHA_1_ALGORITHM); } public static String sha256(String source) { return sha(source, SHA_256_ALGORITHM); } private static String sha(String source, String instance) { MessageDigest md; try { md = MessageDigest.getInstance(instance); md.update(source.getBytes()); return new String(Hex.encodeHex(md.digest())); } catch (NoSuchAlgorithmException e) { return null; } } }

如果需要对 sign 有效期进行校验,需要提供获取服务器时钟的方法,避免因为服务器时间不一致导致的时间差, 此方法可以通过 @Anonymous 去掉验权操作。

@RestController @RequestMapping("common") @Anonymous public class CommonController { /** * 获取系统时间,避免客户端时间不一致 */ @GetMapping("current") public Long current() { return System.currentTimeMillis(); } }

2.4 健康检测

定时用 http get 请求确认网络通畅,如果网络连接失败次数超过阈值,报警给系统管理员

通过 Scheduled 编写定时任务

@Component public class PingCheckTask { private static OkHttpClient okHttpClient = new OkHttpClient(); private Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${ping.service.url}") private String serviceUrl; @Value("${ping.period:3}") private int period; private static Map<String, Integer> errorCalculate = new ConcurrentHashMap<>(); private void resetCount(String url) { errorCalculate.put(url, 0); } private int pushCount(String url) { errorCalculate.put(url, errorCalculate.getOrDefault(url, 0) + 1); return errorCalculate.get(url); } @Scheduled(cron = "0/20 * * * * ?") // 每20秒执行一次 public void scheduler() throws IOException { Request request = new Request.Builder() .url(serviceUrl) .build(); Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { logger.info("ping check {} success!", serviceUrl); resetCount(serviceUrl); } else { logger.info("ping check {} fail! response:{}", serviceUrl, response); int count = pushCount(serviceUrl); if (count > period) { // alert to admin! } } } }

在配置 bean 中需要注入 bean 并允许启动调度

@Configuration @EnableScheduling public class WebConfig implements WebMvcConfigurer { @Bean public PingCheckTask pingCheckTask(){ return new PingCheckTask(); } }

3. 其他

项目写的比较仓促,后续根据实际使用场景进行调整优化。都是站在巨人的肩膀上,利用现成的工具进行拼装。 如果有建议或者希望实现哪些功能,可以留言或者给我提 issue https://github.com/wangyuheng/pharos

春城无处不飞花,寒食东风御柳斜。

推荐大家吃 青团(艾团), 简直发现了新大陆

timgjpeg

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3194 引用 • 8214 回帖
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    132 引用 • 796 回帖
  • 代理
    46 引用 • 103 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...