125 lines
3.6 KiB
Go
125 lines
3.6 KiB
Go
package controllers
|
||
|
||
import (
|
||
"io"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/services/wechatmp"
|
||
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// WechatMpCallbackController 微信公众号服务器回调(微信服务器调用,无需平台鉴权)
|
||
//
|
||
// 公众号后台「设置与开发 → 服务器配置」填写:
|
||
// URL(服务器地址):https://<你的域名>/api/wechat/mp/callback
|
||
// Token:与平台「通知设置 → 微信配置」中的 Token 保持一致
|
||
// 消息加解密方式:明文 / 兼容 / 安全(三种均支持)
|
||
type WechatMpCallbackController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
// Callback 服务器地址验证(GET,回显 echostr)与消息/事件推送(POST,被动回复)
|
||
func (c *WechatMpCallbackController) Callback() {
|
||
cfg, err := wechatmp.LoadEnabledConfig()
|
||
if err != nil {
|
||
c.Ctx.Output.SetStatus(403)
|
||
c.Ctx.Output.Body([]byte("wechat mp not configured"))
|
||
return
|
||
}
|
||
|
||
q := c.Ctx.Request.URL.Query()
|
||
timestamp := q.Get("timestamp")
|
||
nonce := q.Get("nonce")
|
||
|
||
// ---------------- GET:服务器地址校验 ----------------
|
||
if strings.EqualFold(c.Ctx.Input.Method(), "GET") {
|
||
echostr := q.Get("echostr")
|
||
if cfg.EncryptMode == wechatmp.EncryptModeSafe {
|
||
if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, echostr, q.Get("msg_signature")) {
|
||
c.Ctx.Output.SetStatus(403)
|
||
c.Ctx.Output.Body([]byte("invalid signature"))
|
||
return
|
||
}
|
||
plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, echostr)
|
||
if derr != nil {
|
||
c.Ctx.Output.SetStatus(403)
|
||
c.Ctx.Output.Body([]byte("decrypt failed"))
|
||
return
|
||
}
|
||
c.Ctx.Output.Body([]byte(plain))
|
||
return
|
||
}
|
||
if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) {
|
||
c.Ctx.Output.SetStatus(403)
|
||
c.Ctx.Output.Body([]byte("invalid signature"))
|
||
return
|
||
}
|
||
c.Ctx.Output.Body([]byte(echostr))
|
||
return
|
||
}
|
||
|
||
// ---------------- POST:消息 / 事件 ----------------
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
rawBody := string(body)
|
||
if strings.TrimSpace(rawBody) == "" {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
|
||
needEncryptReply := false
|
||
if strings.Contains(rawBody, "<Encrypt>") {
|
||
// 安全/兼容模式:外层为加密 XML,先验签再解密
|
||
outer, perr := wechatmp.ParseInboundXML(rawBody)
|
||
if perr != nil {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, outer.Encrypt, q.Get("msg_signature")) {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, outer.Encrypt)
|
||
if derr != nil {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
rawBody = plain
|
||
needEncryptReply = true
|
||
} else if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) {
|
||
// 明文模式:签名校验不通过直接拒绝(微信会重试)
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
|
||
msg, merr := wechatmp.ParseInboundXML(rawBody)
|
||
if merr != nil {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
|
||
reply := wechatmp.HandleInbound(cfg, msg)
|
||
if strings.TrimSpace(reply) == "" {
|
||
// 无需回复:返回空串,微信视为处理成功
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
plainReply := wechatmp.BuildTextReply(msg.FromUserName, msg.ToUserName, reply, now)
|
||
c.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8")
|
||
if needEncryptReply {
|
||
ts := strconv.FormatInt(now, 10)
|
||
enc, eerr := wechatmp.BuildEncryptedReply(cfg, plainReply, ts, nonce)
|
||
if eerr != nil {
|
||
c.Ctx.Output.Body([]byte(""))
|
||
return
|
||
}
|
||
c.Ctx.Output.Body([]byte(enc))
|
||
return
|
||
}
|
||
c.Ctx.Output.Body([]byte(plainReply))
|
||
}
|