基于 GoLang 快速开发赋能微信公众号
0x00 背景
平时,周围人甚至包括自己都遇到使用idea、pycharm等工具时没有激活码,所以只能求助Goole或者别人的情况。后来,入门学习了一段Go语言之后,我发现基于Gin框架,可以非常快地实现公众号自动回复信息的开发者功能,动态获取激活码,遇到的网络请求,并发FUZZ等问题,Go处理起来也还可以。出于记录下自己的实践过程目的,故作本文以分享。
0x01 功能需求定位
需求实现效果如下:
1)用户通过关注公众号才能实现对话(微信自实现)
2)用户输入非 <idea> 关键字内容时,公众号需要回复 [其他类型信息] xxx
3)用户输入含 <idea>关键字的内容,公众号需要回复 [激活码获取] xxx
0x02 初步平台调研
1)第一步当然是申请开通个人公众号的过程,过程并不复杂,故不在此赘述。
2)第二步是查看微信公众号的开发者功能支持,打开 左侧菜单栏 -> 打开设置与开发 -> 选择[接口权限],显示支持自动回复文本信息,这基本满足我的需求。
0x03 微信开发者配置
由于目标实现并不复杂,流程只需遵循开发者入门指引走如下几步即可.
1)基本配置:选用token时为了安全性和方便性,可以考虑md5进行加密: echo -en "iamtokenhaha"|md5
& 明文模式
2)服务器需要通过验证才能完成基本配置, 分享一些调试的技巧,服务器可以先 nc 监听查看一下微信服务器发过来的请求格式
相关核心代码
1)请求参数结构体,
1type CheckSignatureRequest struct {
2 Signature string `form:"signature"`
3 Timestamp string `form:"timestamp"`
4 Nonce string `form:"nonce"`
5 Echostr string `form:"echostr"`
6}
2)sha1 加密函数实现
1func makeSignature(token, timestamp, nonce string) string { //本地计算signature
2 si := []string{token, timestamp, nonce}
3 sort.Strings(si) //字典序排序
4 str := strings.Join(si, "") //组合字符串
5 s := sha1.New() //返回一个新的使用SHA1校验的hash.Hash接口
6 _, err := io.WriteString(s, str)
7 if err != nil {
8 log.Printf("Error, makeSignature: %s", err)
9 return ""
10 } //WriteString函数将字符串数组str中的内容写入到s中
11 return fmt.Sprintf("%x", s.Sum(nil))
12}
3)验证接口实现, 实现的内容就是将 之前开发者配置的token 和 解析传入的timestamp
nonce
进行字典序排序,然后使用 sha1 进行加密,与传入的signature
比较,确保通信双方都是已知Token的人,最终输出传入的echostr
参数,代表检验成功完成验证。
0x04 信息接收与回复
首先需要了解微信的自动回复机制, 可以简单处理为两个步骤,先接收信息,然后回复信息,定义相关请求结构体
相关资料: 接收普通消息
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
1type WXTextMsg struct {
2 ToUserName string
3 FromUserName string
4 CreateTime int64
5 MsgType string
6 Content string
7 MsgId int64
8}
相关资料: 被动回复用户消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
1type WXRepTextMsg struct {
2 ToUserName string
3 FromUserName string
4 CreateTime int64
5 MsgType string
6 Content string
7 // 若不标记XMLName, 则解析后的xml名为该结构体的名称, 因为需要返回<xml>xxxx</xml>的格式,所以需要标记根节点名称
8 XMLName xml.Name `xml:"xml"`
9}
相关接口代码简化实现如下:
1)处理信息接收
1func wxMsgHandler(context *gin.Context) {
2 var textMsg WXTextMsg
3 // 解析请求内容
4 err := context.ShouldBindXML(&textMsg)
5 if err != nil {
6 log.Printf("[消息接收] - XML数据包解析失败: %vn", err)
7 return
8 }
9 log.Printf("[消息接收] - 收到消息, 消息类型为: %s, 消息内容为: %sn", textMsg.MsgType, textMsg.Content)
10 WXMsgReply(context, textMsg.ToUserName, textMsg.FromUserName, "信息类型", "我是信息内容")
11}
2)处理回复信息,主要获取调换一些发信人和收信人,即 ToUserName
& FromUserName
1func WXMsgReply(c *gin.Context, fromUser, toUser string, mType string, content string) {
2 repTextMsg := WXRepTextMsg{
3 ToUserName: toUser,
4 FromUserName: fromUser,
5 CreateTime: time.Now().Unix(),
6 MsgType: "text",
7 Content: fmt.Sprintf("[%s] - %s n%s", mType, time.Now().Format("2006-01-02"), content),
8 }
9
10 msg, err := xml.Marshal(&repTextMsg)
11 if err != nil {
12 log.Printf("[消息回复] - 将对象进行XML编码出错: %vn", err)
13 return
14 }
15 _, _ = c.Writer.Write(msg)
16}
0x05 激活码实时获取
动态实时获取激活码, 是本程序最核心的一个功能,实现方面稍微走了点弯路,原因是回复信息的内容有长度限制,所以不能直接返回激活码,故需要利用一个中转的接口,以网页的形式展示出来,这里涉及到一个动态路由及其路由有效时间的简易实现。
如果不设置为动态路由,那么用户可以通过直接访问你的固定接口,就可以跳过公众号渠道,直接获取到邀请码
设计实现较为简单,相关代码如下所示:
动态路由通过var uuidRouteList = make(map[string]int)
map映射类型存储, 过期时间通过go-cache
缓存库实现路由失效
Bug: 路由过期的时候并不会删除,因为并没有实现定时任务每3分钟删除一次,而是尝试生成路由的时候,检查一次缓存的方式来刷新过期路由
1)程序通过定时器,每3小时调用一次激活码动态获取函数, updateActivationCodes
函数提供了两种方式自动获取激活码,一旦成功获取到激活码,就会将这个值进行缓存,因为激活码有效期和支持激活的用户是多个的,通过缓存的方式可以提供程序返回信息的速度,减少请求压力
1func RunPeriodically(c *cache.Cache, runChan chan struct{}) {
2 // 设置定时器, 每三小时执行一次, 首先需要执行第一次
3 cdKey, err := getOfficialCode()
4 if err != nil {
5 updateActivationCodes(c, "")
6 } else {
7 updateActivationCodes(c, cdKey)
8 }
9 ticker := time.NewTicker(time.Hour * 3)
10 defer ticker.Stop()
11 for {
12 select {
13 case <-ticker.C:
14 cdKey, err = getOfficialCode()
15 if err != nil {
16 updateActivationCodes(c, "")
17 } else {
18 updateActivationCodes(c, cdKey)
19 }
20 // 控制定时器结束
21 case <-runChan:
22 return
23 }
24 }
25}
2)动态路由实现代码, 每次用户发起请求的时候,会自动生成一个uuid的路由存储在 uuidRouteList
路由列表里面,有效的话,则成功返回激活码
1r.GET("/code/:uuid", showCodeHandler)
1func showCodeHandler(context *gin.Context) {
2 checkUUid := context.Param("uuid")
3 if _, exist := uuidRouteList[checkUUid]; exist {
4 if code, found := caching.Get("code"); found {
5 successCode := code.(string)
6 context.JSON(http.StatusOK, gin.H{
7 "code": 0,
8 "data": successCode,
9 "msg": "success",
10 })
11 } else {
12 context.JSON(http.StatusOK, gin.H{
13 "code": -1,
14 "data": nil,
15 "msg": "not found code in the cache, come back later for few minutes",
16 })
17 }
18 } else {
19 context.JSON(http.StatusForbidden, gin.H{
20 "code": -1,
21 "msg": "Don't Hack me, thanks!",
22 })
23 }
24}
0x06 完整效果展示
1)公众号回复 "idea" 关键字即可获取到一个动态路由链接,访问即可获取到data
字段里面的邀请码,邀请码针对JetBrainsIntellij全家桶来说都是通用的,不单单是idea
2) 浏览器访问打开,即可获取到邀请码
3)当超过三分钟之后,还有人触发动态路由生成,会对过期的路由进行删除处理,从而限制用户复用URL来获得激活码.
0x07 结语
在实现这些简单的功能过程中,其实自己不仅更为熟悉应用Go语法, 而且对于一些实际场景的实现也做了一些自己理解上面的实现。简单来说,Go满足了一个脚本小子绝大部分的期待吧,它表现优异的网络性能、跨平台开箱即用的特性,以及到一开始我觉得对于Py而言有点反人类的语法,但现在觉得GO的语法特点应该定义为简洁明确的人来说,我没有理由不去更多地去尝试它,这仅仅是一个起步,未来,Go应该会成为自己主力的一门语言之一。
原文始发于微信公众号(黑客真酷):基于 GoLang 快速开发赋能微信公众号
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论