@@ -1,19 +1,24 @@ | |||
package hdl | |||
import ( | |||
"applet/app/e" | |||
"applet/app/lib/wechat" | |||
"applet/app/md" | |||
"applet/app/utils" | |||
"applet/app/utils/cache" | |||
db "code.fnuoos.com/zhimeng/model.git/src" | |||
"code.fnuoos.com/zhimeng/model.git/src/super/implement" | |||
"encoding/xml" | |||
"fmt" | |||
"github.com/gin-gonic/gin" | |||
"io/ioutil" | |||
"net/http" | |||
"net/url" | |||
) | |||
type ReqWxMessage struct { | |||
AppID string `xml:"AppId"` | |||
CreateTime int64 `xml:"CreateTime"` | |||
InfoType string `xml:"InfoType"` | |||
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"` | |||
type OriginalWxMessage struct { | |||
AppID string `xml:"AppId"` | |||
Encrypt string `xml:"Encrypt"` | |||
} | |||
func SetTicket(c *gin.Context) { | |||
@@ -26,7 +31,7 @@ func SetTicket(c *gin.Context) { | |||
utils.FilePutContents("SetTicket_Get", utils.SerializeStr(params)) | |||
var wxMsg ReqWxMessage | |||
var originalWxMessage OriginalWxMessage | |||
// 读取请求体 | |||
body, err := ioutil.ReadAll(c.Request.Body) | |||
if err != nil { | |||
@@ -35,18 +40,139 @@ func SetTicket(c *gin.Context) { | |||
} | |||
utils.FilePutContents("SetTicket_Post", string(body)) | |||
err = xml.Unmarshal(body, &wxMsg) | |||
err = xml.Unmarshal(body, &originalWxMessage) | |||
if err != nil { | |||
fmt.Println("setTicket>>>>>>>>", err.Error()) | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
utils.FilePutContents("SetTicket_XML", utils.SerializeStr(wxMsg)) | |||
//1、查找对应 wx_open_third_party_app_list 记录 | |||
wxOpenThirdPartyAppListDb := implement.NewWxOpenThirdPartyAppListDb(db.Db) | |||
wxOpenThirdPartyAppList, err := wxOpenThirdPartyAppListDb.GetWxOpenThirdPartyAppListByAppId(originalWxMessage.AppID) | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
if wxOpenThirdPartyAppList == nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": "未查询到对应App记录"}) | |||
return | |||
} | |||
//2、对消息体进行解密 | |||
instance := wechat.NewWechatMsgCrypt(wxOpenThirdPartyAppList.Token, wxOpenThirdPartyAppList.AesKey, wxOpenThirdPartyAppList.Appid) | |||
eventRequest := wechat.EventEncryptRequest{ | |||
XMLName: xml.Name{}, | |||
Encrypt: originalWxMessage.Encrypt, | |||
Appid: originalWxMessage.AppID, | |||
} | |||
reqWxMessage := instance.WechatEventDecrypt(eventRequest, params["msg_signature"], params["timestamp"], params["nonce"]) | |||
fmt.Println("解密结果:", reqWxMessage) | |||
utils.FilePutContents("SetTicket_XML", utils.SerializeStr(reqWxMessage)) | |||
if reqWxMessage.InfoType == "component_verify_ticket" { //TODO::微信公众平台 验证票据 | |||
cacheKey := fmt.Sprintf(md.MasterComponentVerifyTicket, utils.AnyToString(wxOpenThirdPartyAppList.Uuid)) | |||
cacheComponentVerifyTicket, _ := cache.GetString(cacheKey) | |||
if cacheComponentVerifyTicket == "" || cacheComponentVerifyTicket != reqWxMessage.ComponentVerifyTicket { | |||
cache.SetEx(cacheKey, reqWxMessage.ComponentVerifyTicket, 43140) | |||
wxOpenThirdPartyAppList.ComponentVerifyTicket = reqWxMessage.ComponentVerifyTicket | |||
_, err = wxOpenThirdPartyAppListDb.UpdateWxOpenThirdPartyAppList(wxOpenThirdPartyAppList, "component_verify_ticket") | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
} | |||
} | |||
if reqWxMessage.InfoType == "unauthorized" { //TODO::微信公众平台 取消授权 | |||
appid := reqWxMessage.AuthorizerAppid | |||
userWxAppletListDb := implement.NewUserWxAppletListDb(db.Db) | |||
userWxAppletList, err := userWxAppletListDb.GetUserWxAppletListByAppId(appid) | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
if userWxAppletList == nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": "未查询到对应小程序授权记录"}) | |||
return | |||
} | |||
userWxAppletList.IsAuth = 0 | |||
_, err = userWxAppletListDb.UpdateUserWxAppletList(userWxAppletList, "is_auth") | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
} | |||
if reqWxMessage.InfoType == "unauthorized" || reqWxMessage.InfoType == "authorized" { //TODO::微信公众平台 授权 || 更新授权 | |||
appid := reqWxMessage.AuthorizerAppid | |||
userWxAppletListDb := implement.NewUserWxAppletListDb(db.Db) | |||
userWxAppletList, err := userWxAppletListDb.GetUserWxAppletListByAppId(appid) | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
if userWxAppletList == nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": "未查询到对应小程序授权记录"}) | |||
return | |||
} | |||
userWxAppletList.IsAuth = 1 | |||
_, err = userWxAppletListDb.UpdateUserWxAppletList(userWxAppletList, "is_auth") | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
} | |||
c.String(http.StatusOK, "success") | |||
return | |||
} | |||
func GetPreAuthCode(c *gin.Context) { | |||
masterId := c.DefaultQuery("master_id", "") | |||
wxOpenThirdPartyAppListDb := implement.NewWxOpenThirdPartyAppListDb(db.Db) | |||
wxOpenThirdPartyAppList, err := wxOpenThirdPartyAppListDb.GetWxOpenThirdPartyAppList(utils.StrToInt(masterId)) | |||
if err != nil { | |||
return | |||
} | |||
if wxOpenThirdPartyAppList == nil { | |||
e.OutErr(c, e.ERR_NOT_FAN, "未查询到对应三方应用记录") | |||
return | |||
} | |||
wxApiService, err := wechat.NewWxApiService(masterId, wxOpenThirdPartyAppList.Appid, wxOpenThirdPartyAppList.AppSecret) | |||
if err != nil { | |||
e.OutErr(c, e.ERR, err.Error()) | |||
return | |||
} | |||
preAuthCode, err := wxApiService.GetPreAuthCode() | |||
if err != nil { | |||
e.OutErr(c, e.ERR, err.Error()) | |||
return | |||
} | |||
var w http.ResponseWriter | |||
w.Header().Set("Access-Control-Allow-Origin", "*") | |||
redirectURI := "http://super.admin.izhyin.com/Wx/getAuthUrlCallBack" | |||
// 对redirectURI进行URL编码 | |||
encodedRedirectURI := url.QueryEscape(redirectURI) | |||
// 构造微信登录页面的URL | |||
baseURL := "https://mp.weixin.qq.com/cgi-bin/componentloginpage" | |||
query := url.Values{} | |||
query.Add("component_appid", wxOpenThirdPartyAppList.Appid) | |||
query.Add("pre_auth_code", preAuthCode) | |||
query.Add("redirect_uri", encodedRedirectURI) | |||
query.Add("auth_type", "1") | |||
// 将查询参数附加到基础URL | |||
urlStr := baseURL + "?" + query.Encode() | |||
// 设置JavaScript重定向 | |||
fmt.Fprintf(w, ` | |||
<script> | |||
window.onload = function () { | |||
window.location.href = '%s'; | |||
}; | |||
</script>`, urlStr) | |||
} | |||
func WechatMsgRecieve(c *gin.Context) { | |||
return | |||
@@ -0,0 +1,68 @@ | |||
package wechat | |||
import ( | |||
"bytes" | |||
"crypto/sha1" | |||
"fmt" | |||
"io" | |||
"math/rand" | |||
"sort" | |||
"strings" | |||
) | |||
// GetSignature 获取签名 | |||
func GetSignature(timestamp, nonce string, encrypted string, token string) string { | |||
data := []string{ | |||
encrypted, | |||
token, | |||
timestamp, | |||
nonce, | |||
} | |||
sort.Strings(data) | |||
s := sha1.New() | |||
_, err := io.WriteString(s, strings.Join(data, "")) | |||
if err != nil { | |||
panic("签名错误:sign error") | |||
} | |||
return fmt.Sprintf("%x", s.Sum(nil)) | |||
} | |||
func makeRandomString(length int) string { | |||
randStr := "" | |||
strSource := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyl" | |||
maxLength := len(strSource) - 1 | |||
for i := 0; i < length; i++ { | |||
randomNum := rand.Intn(maxLength) | |||
randStr += strSource[randomNum : randomNum+1] | |||
} | |||
return randStr | |||
} | |||
func pKCS7Pad(plainText []byte, blockSize int) []byte { | |||
// block size must be bigger or equal 2 | |||
if blockSize < 1<<1 { | |||
panic("block size is too small (minimum is 2 bytes)") | |||
} | |||
// block size up to 255 requires 1 byte padding | |||
if blockSize < 1<<8 { | |||
// calculate padding length | |||
padLen := padLength(len(plainText), blockSize) | |||
// define PKCS7 padding block | |||
padding := bytes.Repeat([]byte{byte(padLen)}, padLen) | |||
// apply padding | |||
padded := append(plainText, padding...) | |||
return padded | |||
} | |||
// block size bigger or equal 256 is not currently supported | |||
panic("unsupported block size") | |||
} | |||
func padLength(sliceLength, blockSize int) int { | |||
padLen := blockSize - sliceLength%blockSize | |||
if padLen == 0 { | |||
padLen = blockSize | |||
} | |||
return padLen | |||
} |
@@ -0,0 +1,11 @@ | |||
package md | |||
type GetComponentAccessToken struct { | |||
ComponentAccessToken string `json:"component_access_token" example:"第三方平台 access_token"` | |||
ExpiresIn int `json:"expires_in" example:"有效期,单位:秒"` | |||
} | |||
type GetPreAuthCode struct { | |||
PreAuthCode string `json:"pre_auth_code" example:"预授权码"` | |||
ExpiresIn int `json:"expires_in" example:"有效期,单位:秒"` | |||
} |
@@ -0,0 +1,254 @@ | |||
package wechat | |||
import ( | |||
"bytes" | |||
"encoding/binary" | |||
"encoding/xml" | |||
"errors" | |||
"strconv" | |||
"time" | |||
) | |||
var EventTicket = "component_verify_ticket" //ticket推送 | |||
var EventUnauthorized = "unauthorized" //取消授权 | |||
var EventUpdateAuthorized = "updateauthorized" //更新授权 | |||
var EventAuthorized = "authorized" //授权成功 | |||
var MsgTypeText = "text" //文本消息 | |||
var MsgTypeImage = "image" //文本消息 | |||
var MsgTypeVoice = "voice" //语音消息 | |||
var MsgTypeVideo = "Video" //视频消息 | |||
var MsgTypeMusic = "music" //音乐消息 | |||
var MsgTypeNews = "news" //图文消息 | |||
// EventMessageBody 事件推送 | |||
type EventMessageBody struct { | |||
XMLName xml.Name `xml:"xml"` | |||
AppId string `xml:"AppId" json:"app_id"` | |||
CreateTime int `xml:"CreateTime" json:"create_time"` | |||
InfoType string `xml:"InfoType" json:"info_type"` | |||
ComponentVerifyTicket string `xml:"ComponentVerifyTicket" json:"component_verify_ticket"` | |||
AuthorizerAppid string `xml:"AuthorizerAppid" json:"authorizer_appid"` | |||
AuthorizationCode string `xml:"AuthorizationCode" json:"authorization_code"` | |||
AuthorizationCodeExpiredTime string `xml:"AuthorizationCodeExpiredTime" json:"authorization_code_expired_time"` | |||
PreAuthCode string `xml:"PreAuthCode" json:"pre_auth_code"` | |||
} | |||
// MessageBodyDecrypt 消息体 | |||
type MessageBodyDecrypt struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string `xml:"ToUserName"` | |||
FromUserName string `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType string `xml:"MsgType"` | |||
Url string `xml:"Url"` | |||
PicUrl string `xml:"PicUrl"` | |||
MediaId string `xml:"MediaId"` | |||
ThumbMediaId string `xml:"ThumbMediaId"` | |||
Content string `xml:"Content"` | |||
MsgId int `xml:"MsgId"` | |||
Location_X string `xml:"Location_x"` | |||
Location_Y string `xml:"Location_y"` | |||
Label string `xml:"Label"` | |||
} | |||
type MessageEncryptBody struct { | |||
XMLName xml.Name `xml:"xml"` | |||
Encrypt CDATA `xml:"Encrypt"` | |||
MsgSignature CDATA `xml:"MsgSignature"` | |||
TimeStamp string `xml:"TimeStamp"` | |||
Nonce CDATA `xml:"Nonce"` | |||
} | |||
type MessageText struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
Content CDATA `xml:"Content"` | |||
} | |||
type MessageImage struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
Image Media `xml:"Image"` | |||
} | |||
type MessageVoice struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
Voice Media `xml:"Voice"` | |||
} | |||
type MessageVideo struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
Video Video `xml:"Video"` | |||
} | |||
type MessageMusic struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
Music Music `xml:"Music"` | |||
} | |||
type MessageArticle struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName CDATA `xml:"ToUserName"` | |||
FromUserName CDATA `xml:"FromUserName"` | |||
CreateTime string `xml:"CreateTime"` | |||
MsgType CDATA `xml:"MsgType"` | |||
ArticleCount string `xml:"ArticleCount"` | |||
Articles []ArticleItem `xml:"Articles"` | |||
} | |||
type CDATA struct { | |||
Text string `xml:",innerxml"` | |||
} | |||
type Media struct { | |||
MediaId CDATA `xml:"MediaId"` | |||
} | |||
type Video struct { | |||
MediaId CDATA `xml:"MediaId"` | |||
Title CDATA `xml:"Title"` | |||
Description CDATA `xml:"Description"` | |||
} | |||
type Music struct { | |||
Title CDATA `xml:"Title"` | |||
Description CDATA `xml:"Description"` | |||
MusicUrl CDATA `xml:"MusicUrl"` | |||
HQMusicUrl CDATA `xml:"HQMusicUrl"` | |||
ThumbMediaId CDATA `xml:"ThumbMediaId"` | |||
} | |||
type ArticleItem struct { | |||
Title CDATA `xml:"Title"` | |||
Description CDATA `xml:"Description"` | |||
PicUrl CDATA `xml:"PicUrl"` | |||
Url CDATA `xml:"Url"` | |||
} | |||
func FormatMessage(plainText []byte, data interface{}) (*interface{}, error) { | |||
length := GetMessageLength(plainText) | |||
err := xml.Unmarshal(plainText[20:20+length], data) | |||
if err != nil { | |||
return nil, errors.New("格式化消息失败:format message error") | |||
} | |||
return &data, nil | |||
} | |||
func GetMessageLength(plainText []byte) int32 { | |||
// Read length | |||
buf := bytes.NewBuffer(plainText[16:20]) | |||
var length int32 | |||
err := binary.Read(buf, binary.BigEndian, &length) | |||
if err != nil { | |||
panic("获取消息长度失败:read message length error") | |||
} | |||
return length | |||
} | |||
func ValueToCDATA(content string) CDATA { | |||
return CDATA{"<![CDATA[" + content + "]]>"} | |||
} | |||
// FormatTextMessage 格式化文本消息 | |||
func FormatTextMessage(fromUserName string, toUserName string, content string) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
textMessage := MessageText{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), Content: ValueToCDATA(content), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeText)} | |||
messageBytes, err := xml.MarshalIndent(textMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化文本消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatImageMessage 格式化图片消息 | |||
func FormatImageMessage(fromUserName string, toUserName string, mediaId string) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
imageMessage := MessageImage{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeImage), Image: Media{ValueToCDATA(mediaId)}} | |||
messageBytes, err := xml.MarshalIndent(imageMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化图片消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatVoiceMessage 格式语音消息 | |||
func FormatVoiceMessage(fromUserName string, toUserName string, mediaId string) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
voiceMessage := MessageVoice{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeVoice), Voice: Media{ValueToCDATA(mediaId)}} | |||
messageBytes, err := xml.MarshalIndent(voiceMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化语音消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatVideoMessage 格式化视频消息 | |||
func FormatVideoMessage(fromUserName string, toUserName string, mediaId string, title string, description string) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
videoMessage := MessageVideo{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeVideo), Video: Video{ | |||
MediaId: ValueToCDATA(mediaId), | |||
Title: ValueToCDATA(title), | |||
Description: ValueToCDATA(description), | |||
}} | |||
messageBytes, err := xml.MarshalIndent(videoMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化语音消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatMusicMessage 格式化音乐消息 | |||
func FormatMusicMessage(fromUserName string, toUserName string, thumbMediaId string, title string, description string, musicUrl string, hQMusicUrl string) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
musicMessage := MessageMusic{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeMusic), Music: Music{ | |||
Title: ValueToCDATA(title), | |||
Description: ValueToCDATA(description), | |||
MusicUrl: ValueToCDATA(musicUrl), | |||
HQMusicUrl: ValueToCDATA(hQMusicUrl), | |||
ThumbMediaId: ValueToCDATA(thumbMediaId), | |||
}} | |||
messageBytes, err := xml.MarshalIndent(musicMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化音乐消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatArticlesMessage 格式化音乐消息 | |||
func FormatArticlesMessage(fromUserName string, toUserName string, items []ArticleItem) []byte { | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
articleNum := strconv.Itoa(len(items)) | |||
musicMessage := MessageArticle{FromUserName: ValueToCDATA(fromUserName), ToUserName: ValueToCDATA(toUserName), CreateTime: timestamp, MsgType: ValueToCDATA(MsgTypeNews), Articles: items, ArticleCount: articleNum} | |||
messageBytes, err := xml.MarshalIndent(musicMessage, " ", " ") | |||
if err != nil { | |||
panic("格式化图文消息失败:xml marsha1 error;" + err.Error()) | |||
} | |||
return messageBytes | |||
} | |||
// FormatEncryptData 格式化微信加密数据 | |||
func FormatEncryptData(encrypted string, token string) MessageEncryptBody { | |||
nonce := makeRandomString(16) | |||
timestamp := strconv.Itoa(int(time.Now().Unix())) | |||
data := MessageEncryptBody{Encrypt: ValueToCDATA(encrypted), Nonce: ValueToCDATA(nonce), MsgSignature: ValueToCDATA(GetSignature(timestamp, nonce, encrypted, token)), TimeStamp: timestamp} | |||
return data | |||
} |
@@ -0,0 +1,61 @@ | |||
package wechat | |||
import ( | |||
"crypto/aes" | |||
"crypto/cipher" | |||
"crypto/rand" | |||
"encoding/base64" | |||
"io" | |||
) | |||
type prpCrypt struct { | |||
Key []byte | |||
Iv []byte | |||
} | |||
func NewPrpCrypt(aesKey string) *prpCrypt { | |||
instance := new(prpCrypt) | |||
//网络字节序 | |||
instance.Key, _ = base64.StdEncoding.DecodeString(aesKey + "=") | |||
instance.Iv = randomIv() | |||
return instance | |||
} | |||
func randomIv() []byte { | |||
iv := make([]byte, aes.BlockSize) | |||
if _, err := io.ReadFull(rand.Reader, iv); err != nil { | |||
panic("random iv error") | |||
} | |||
return iv | |||
} | |||
func (prp *prpCrypt) decrypt(encrypted string) ([]byte, error) { | |||
encryptedBytes, _ := base64.StdEncoding.DecodeString(encrypted) | |||
k := len(prp.Key) //PKCS#7 | |||
if len(encryptedBytes)%k != 0 { | |||
panic("ciphertext size is not multiple of aes key length") | |||
} | |||
block, err := aes.NewCipher(prp.Key) | |||
if err != nil { | |||
return nil, err | |||
} | |||
blockMode := cipher.NewCBCDecrypter(block, prp.Iv) | |||
plainText := make([]byte, len(encryptedBytes)) | |||
blockMode.CryptBlocks(plainText, encryptedBytes) | |||
return plainText, nil | |||
} | |||
func (prp *prpCrypt) encrypt(plainText []byte) ([]byte, error) { | |||
k := len(prp.Key) | |||
if len(plainText)%k != 0 { | |||
plainText = pKCS7Pad(plainText, k) | |||
} | |||
block, err := aes.NewCipher(prp.Key) | |||
if err != nil { | |||
return nil, err | |||
} | |||
cipherData := make([]byte, len(plainText)) | |||
blockMode := cipher.NewCBCEncrypter(block, prp.Iv) | |||
blockMode.CryptBlocks(cipherData, plainText) | |||
return cipherData, nil | |||
} |
@@ -0,0 +1,109 @@ | |||
package wechat | |||
import ( | |||
"applet/app/cfg" | |||
md2 "applet/app/lib/wechat/md" | |||
"applet/app/md" | |||
"applet/app/utils" | |||
"applet/app/utils/cache" | |||
db "code.fnuoos.com/zhimeng/model.git/src" | |||
"code.fnuoos.com/zhimeng/model.git/src/super/implement" | |||
"encoding/json" | |||
"errors" | |||
"fmt" | |||
) | |||
type WxApiService struct { | |||
ComponentAppid string `json:"component_appid"` | |||
ComponentAppsecret string `json:"component_appsecret"` | |||
ComponentVerifyTicket string `json:"component_verify_ticket"` | |||
MasterId string `json:"master_id"` | |||
Host string `json:"host"` | |||
} | |||
func NewWxApiService(masterId, componentAppid, componentAppsecret string) (wxApiService WxApiService, err error) { // set方法 | |||
wxApiService.MasterId = masterId | |||
wxApiService.ComponentAppid = componentAppid | |||
wxApiService.ComponentAppsecret = componentAppsecret | |||
wxApiService.Host = "http://super.advertisement.dengbiao.top" | |||
if cfg.Prd { | |||
wxApiService.Host = "http://www.baidu.com" | |||
} | |||
cacheKey := fmt.Sprintf(md.MasterComponentVerifyTicket, wxApiService.MasterId) | |||
cacheComponentVerifyTicket, _ := cache.GetString(cacheKey) | |||
if cacheComponentVerifyTicket != "" { | |||
return wxApiService, errors.New("微信验证票据ticket未获取到") | |||
} | |||
return | |||
} | |||
// GetComponentAccessToken 获取接口调用令牌token | |||
func (wxApiService *WxApiService) GetComponentAccessToken() (cacheComponentAccessToken string, err error) { // set方法 | |||
cacheKey := fmt.Sprintf(md.MasterComponentAccessToken, wxApiService.MasterId) | |||
cacheComponentAccessToken, _ = cache.GetString(cacheKey) | |||
if cacheComponentAccessToken != "" { | |||
url := "https://api.weixin.qq.com/cgi-bin/component/api_component_token" | |||
params := map[string]string{ | |||
"component_appid": wxApiService.ComponentAppid, | |||
"component_appsecret": wxApiService.ComponentAppsecret, | |||
"component_verify_ticket": wxApiService.ComponentVerifyTicket, | |||
} | |||
postBody, err1 := utils.CurlPost(url, params, nil) | |||
if err1 != nil { | |||
return cacheComponentAccessToken, err1 | |||
} | |||
var postData md2.GetComponentAccessToken | |||
err = json.Unmarshal(postBody, &postData) | |||
if err != nil { | |||
return | |||
} | |||
if postData.ComponentAccessToken == "" { | |||
err = errors.New("获取接口令牌token失败") | |||
return | |||
} | |||
cacheComponentAccessToken = postData.ComponentAccessToken | |||
cache.SetEx(cacheKey, cacheComponentAccessToken, postData.ExpiresIn-600) | |||
wxOpenThirdPartyAppListDb := implement.NewWxOpenThirdPartyAppListDb(db.Db) | |||
wxOpenThirdPartyAppList, err1 := wxOpenThirdPartyAppListDb.GetWxOpenThirdPartyAppListByAppId(wxApiService.ComponentAppid) | |||
if err1 != nil { | |||
return cacheComponentAccessToken, err1 | |||
} | |||
if wxOpenThirdPartyAppList == nil { | |||
err = errors.New("未查询到对应App记录") | |||
return | |||
} | |||
wxOpenThirdPartyAppList.ComponentAccessToken = cacheComponentAccessToken | |||
_, err = wxOpenThirdPartyAppListDb.UpdateWxOpenThirdPartyAppList(wxOpenThirdPartyAppList, "component_access_token") | |||
if err != nil { | |||
return | |||
} | |||
} | |||
return | |||
} | |||
// GetPreAuthCode 获取预授权码 | |||
func (wxApiService *WxApiService) GetPreAuthCode() (preAuthCode string, err error) { // set方法 | |||
componentAccessToken, err := wxApiService.GetComponentAccessToken() | |||
if err != nil { | |||
return | |||
} | |||
url := "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=" + componentAccessToken | |||
params := map[string]string{ | |||
"component_appid": wxApiService.ComponentAppid, | |||
} | |||
postBody, err := utils.CurlPost(url, params, nil) | |||
if err != nil { | |||
return | |||
} | |||
var postData md2.GetPreAuthCode | |||
err = json.Unmarshal(postBody, &postData) | |||
if err != nil { | |||
return | |||
} | |||
if postData.PreAuthCode == "" { | |||
err = errors.New("获取预授权码失败") | |||
return | |||
} | |||
preAuthCode = postData.PreAuthCode | |||
return | |||
} |
@@ -0,0 +1,11 @@ | |||
package wechat | |||
var SuccessCode = 0 | |||
var ValidateSignatureError = -40001 | |||
var ParseXmlError = -40002 | |||
var ComputeSignatureError = -40003 | |||
var IllegalAesKey = -40004 | |||
var ValidateAppidError = -40005 | |||
var EncryptAESError = -40006 | |||
var DecryptAESError = -40007 | |||
var IllegalBuffer = -40008 |
@@ -0,0 +1,200 @@ | |||
package wechat | |||
import ( | |||
"bytes" | |||
"encoding/base64" | |||
"encoding/binary" | |||
"encoding/xml" | |||
"errors" | |||
"fmt" | |||
) | |||
type msgCrypt struct { | |||
token string | |||
aesKey string | |||
appid string | |||
} | |||
// EventEncryptRequest 微信事件推送结构体 | |||
type EventEncryptRequest struct { | |||
XMLName xml.Name `xml:"xml"` | |||
Encrypt string `xml:"Encrypt"` | |||
Appid string `xml:"Appid"` | |||
} | |||
// MessageEncryptRequest 微信消息加密结构体 | |||
type MessageEncryptRequest struct { | |||
XMLName xml.Name `xml:"xml"` | |||
Encrypt string `xml:"Encrypt"` | |||
MsgSignature string `xml:"MsgSignature"` | |||
TimeStamp string `xml:"TimeStamp"` | |||
Nonce string `xml:"Nonce"` | |||
} | |||
// NewWechatMsgCrypt 实例化微信加解密 | |||
func NewWechatMsgCrypt(token string, aesKey string, appid string) *msgCrypt { | |||
instance := new(msgCrypt) | |||
instance.token = token | |||
instance.aesKey = aesKey | |||
instance.appid = appid | |||
return instance | |||
} | |||
// WechatEventDecrypt 微信事件推送解密 | |||
func (w *msgCrypt) WechatEventDecrypt(eventRequest EventEncryptRequest, msgSignature string, timestamp, nonce string) EventMessageBody { | |||
errCode, data := w.decryptMsg(msgSignature, timestamp, nonce, eventRequest.Encrypt) | |||
if errCode != SuccessCode { | |||
panic(fmt.Sprintf("消息解密失败,code:%d", errCode)) | |||
} | |||
message := EventMessageBody{} | |||
_, err := FormatMessage(data, &message) | |||
if err != nil { | |||
panic(fmt.Sprintf("消息格式化失败,%s", err.Error())) | |||
} | |||
return message | |||
} | |||
// WechatMessageDecrypt 微信消息解密 | |||
func (w *msgCrypt) WechatMessageDecrypt(messageEncryptRequest MessageEncryptRequest) interface{} { | |||
errCode, data := w.decryptMsg(messageEncryptRequest.MsgSignature, messageEncryptRequest.TimeStamp, messageEncryptRequest.Nonce, messageEncryptRequest.Encrypt) | |||
if errCode != SuccessCode { | |||
panic(fmt.Sprintf("消息解密失败,code:%d", errCode)) | |||
} | |||
message := MessageBodyDecrypt{} | |||
_, err := FormatMessage(data, &message) | |||
if err != nil { | |||
panic(fmt.Sprintf("消息格式化失败,%s", err.Error())) | |||
} | |||
return message | |||
} | |||
// WechatTextMessage 微信文本消息加密 | |||
func (w *msgCrypt) WechatTextMessage(fromUserName string, toUserName string, content string) MessageEncryptBody { | |||
message := FormatTextMessage(fromUserName, toUserName, content) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// WechatImageMessage 微信图片消息加密 | |||
func (w *msgCrypt) WechatImageMessage(fromUserName string, toUserName string, mediaId string) MessageEncryptBody { | |||
message := FormatImageMessage(fromUserName, toUserName, mediaId) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// WechatVoiceMessage 微信语音消息加密 | |||
func (w *msgCrypt) WechatVoiceMessage(fromUserName string, toUserName string, mediaId string) MessageEncryptBody { | |||
message := FormatVoiceMessage(fromUserName, toUserName, mediaId) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// WechatVideoMessage 微信视频消息加密 | |||
func (w *msgCrypt) WechatVideoMessage(fromUserName string, toUserName string, mediaId string, title string, description string) MessageEncryptBody { | |||
message := FormatVideoMessage(fromUserName, toUserName, mediaId, title, description) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// WechatMusicMessage 微信音乐消息加密 | |||
func (w *msgCrypt) WechatMusicMessage(fromUserName string, toUserName string, thumbMediaId string, title string, description string, musicUrl string, hQMusicUrl string) MessageEncryptBody { | |||
message := FormatMusicMessage(fromUserName, toUserName, thumbMediaId, title, description, musicUrl, hQMusicUrl) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// WechatArticlesMessage 微信图文消息加密 | |||
func (w *msgCrypt) WechatArticlesMessage(fromUserName string, toUserName string, items []ArticleItem) MessageEncryptBody { | |||
message := FormatArticlesMessage(fromUserName, toUserName, items) | |||
encrypted, err := w.encryptMsg(message) | |||
if err != nil { | |||
panic("消息加密失败:" + err.Error()) | |||
} | |||
data := FormatEncryptData(encrypted, w.token) | |||
return data | |||
} | |||
// decryptMsg aes消息解密 | |||
func (w *msgCrypt) decryptMsg(msgSignature string, timestamp, nonce string, encrypted string) (int, []byte) { | |||
//验证aes | |||
if len(w.aesKey) != 43 { | |||
return IllegalAesKey, nil | |||
} | |||
//判断签名是否一致 | |||
if err := w.validSignature(msgSignature, timestamp, nonce, encrypted); err != nil { | |||
return ValidateSignatureError, nil | |||
} | |||
//解密 | |||
prp := NewPrpCrypt(w.aesKey) | |||
plainText, err := prp.decrypt(encrypted) | |||
if err != nil { | |||
return DecryptAESError, nil | |||
} | |||
//验证appid是否一致(消息来源是否一致) | |||
if err := w.validMessageSource(plainText); err != nil { | |||
return ValidateAppidError, nil | |||
} | |||
return SuccessCode, plainText | |||
} | |||
// encryptMsg aes消息加密 | |||
func (w *msgCrypt) encryptMsg(message []byte) (string, error) { | |||
//计算消息长度 | |||
buf := new(bytes.Buffer) | |||
err := binary.Write(buf, binary.BigEndian, int32(len(message))) | |||
if err != nil { | |||
return "", err | |||
} | |||
messageLength := buf.Bytes() | |||
//生成随机字符串 | |||
randBytes := []byte(makeRandomString(16)) | |||
plainData := bytes.Join([][]byte{randBytes, messageLength, message, []byte(w.appid)}, nil) | |||
prp := NewPrpCrypt(w.aesKey) | |||
//消息加密 | |||
encrypted, err := prp.encrypt(plainData) | |||
if err != nil { | |||
return "", err | |||
} | |||
return base64.StdEncoding.EncodeToString(encrypted), nil | |||
} | |||
// validSignature 验证签名是否一致 | |||
func (w *msgCrypt) validSignature(msgSignature string, timestamp, nonce string, encrypted string) error { | |||
validSignature := GetSignature(timestamp, nonce, encrypted, w.token) | |||
if validSignature != msgSignature { | |||
return errors.New("签名不一致:valid sign error") | |||
} | |||
return nil | |||
} | |||
// validMessageSource 验证消息来源 | |||
func (w *msgCrypt) validMessageSource(plainText []byte) error { | |||
messageLength := GetMessageLength(plainText) | |||
//获取appid位置 | |||
appIdStartPos := 20 + messageLength | |||
id := plainText[appIdStartPos : int(appIdStartPos)+len(w.appid)] | |||
if string(id) != w.appid { | |||
return errors.New("消息来源不一致:Appid is invalid") | |||
} | |||
return nil | |||
} |
@@ -15,4 +15,7 @@ const ( | |||
KEY_SYS_CFG_CACHE = "sys_cfg_cache" | |||
CfgCacheTime = 86400 | |||
MasterComponentVerifyTicket = "master_component_verify_ticket:%s" | |||
MasterComponentAccessToken = "master_component_access_token:%s" | |||
) |
@@ -51,10 +51,11 @@ func Init() *gin.Engine { | |||
func route(r *gin.RouterGroup) { | |||
r.GET("/test", hdl.Demo) | |||
wxOpenNotify := r.Group("/wxOpenNotify") | |||
wxOpenNotify := r.Group("/wxOpen") | |||
{ | |||
wxOpenNotify.Any("/:APPID/wechatMsgRecieve", hdl.WechatMsgRecieve) | |||
wxOpenNotify.Any("/setTicket", hdl.SetTicket) | |||
wxOpenNotify.GET("/getPreAuthCode", hdl.GetPreAuthCode) | |||
} | |||
r.Use(mw.DB) // 以下接口需要用到数据库 | |||
@@ -5,7 +5,7 @@ go 1.18 | |||
//replace code.fnuoos.com/zhimeng/model.git => E:/company/ad/models | |||
require ( | |||
code.fnuoos.com/zhimeng/model.git v0.0.3-0.20240819041103-0f30b4455fec | |||
code.fnuoos.com/zhimeng/model.git v0.0.3-0.20240820125928-6ce4c06dda24 | |||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 | |||
github.com/boombuler/barcode v1.0.1 | |||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 | |||