diff --git a/platformapi/weixinapi/weixinapi.go b/platformapi/weixinapi/weixinapi.go index cb26739e..2599cb0b 100644 --- a/platformapi/weixinapi/weixinapi.go +++ b/platformapi/weixinapi/weixinapi.go @@ -1,6 +1,8 @@ package weixinapi import ( + "crypto/aes" + "crypto/cipher" "net/http" "strings" "sync" @@ -10,7 +12,7 @@ import ( ) const ( - prodURL = "https://api.weixin.qq.com/cgi-bin" + prodURL = "https://api.weixin.qq.com" ) const ( @@ -41,6 +43,32 @@ type API struct { locker sync.RWMutex } +type SNSTokenInfo struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + OpenID string `json:"openid"` + Scope string `json:"scope"` +} + +type SNSUserInfo struct { + OpenID string `json:"openid"` + NickName string `json:"nickname"` + Sex int `json:"sex"` + Province string `json:"province"` + City string `json:"city"` + Country string `json:"country"` + HeadImgURL string `json:"headimgurl"` + Privilege interface{} `json:"privilege"` + UnionID string `json:"unionid"` +} + +type SessionInfo struct { + OpenID string `json:"openid"` + SessionKey string `json:"session_key"` + UnionID string `json:"unionid"` +} + func New(appID, secret string, config ...*platformapi.APIConfig) *API { curConfig := platformapi.DefAPIConfig if len(config) > 0 { @@ -79,6 +107,10 @@ func (a *API) GetToken() string { return a.token } +func isSNSAction(action string) bool { + return strings.Index(action, "sns/") == 0 +} + func (a *API) AccessAPI(action string, params map[string]interface{}, body string) (retVal map[string]interface{}, err error) { if params != nil && body != "" { panic("params and body can not all non-empty") @@ -87,16 +119,18 @@ func (a *API) AccessAPI(action string, params map[string]interface{}, body strin for k, v := range params { params2[k] = v } - if action == actionGetToken { + + if params2["grant_type"] != nil { params2["appid"] = a.appID params2["secret"] = a.secret - } else { + } else if !isSNSAction(action) { accessToken := a.GetToken() if accessToken == "" { panic("token is empty!") } params2["access_token"] = accessToken } + fullURL := utils.GenerateGetURL(prodURL, action, params2) // baseapi.SugarLogger.Debug(fullURL) @@ -143,7 +177,7 @@ func (a *API) AccessAPI(action string, params map[string]interface{}, body strin } func (a *API) RefreshToken() (tokenInfo *TokenInfo, err error) { - result, err := a.AccessAPI(actionGetToken, utils.Params2Map("grant_type", "client_credential"), "") + result, err := a.AccessAPI("cgi-bin/token", utils.Params2Map("grant_type", "client_credential"), "") if err != nil { return nil, err } @@ -169,6 +203,95 @@ func (a *API) MessageTemplateSend(userOpneID, templateID, downloadURL string, mi if miniProgram != nil { bodyJson["miniprogram"] = miniProgram } - _, err = a.AccessAPI("message/template/send", nil, string(utils.MustMarshal(bodyJson))) + _, err = a.AccessAPI("cgi-bin/message/template/send", nil, string(utils.MustMarshal(bodyJson))) return err } + +func mapData2SNSToken(result map[string]interface{}) *SNSTokenInfo { + return &SNSTokenInfo{ + AccessToken: utils.Interface2String(result["access_token"]), + ExpiresIn: int(utils.MustInterface2Int64(result["expires_in"])), + RefreshToken: utils.Interface2String(result["refresh_token"]), + OpenID: utils.Interface2String(result["openid"]), + Scope: utils.Interface2String(result["scope"]), + } +} + +func (a *API) SNSGetToken(code string) (tokenInfo *SNSTokenInfo, err error) { + result, err := a.AccessAPI("sns/oauth2/access_token", utils.Params2Map("grant_type", "authorization_code", "code", code), "") + if err != nil { + return nil, err + } + return mapData2SNSToken(result), nil +} + +func (a *API) SNSRefreshToken(refreshToken string) (tokenInfo *SNSTokenInfo, err error) { + result, err := a.AccessAPI("sns/oauth2/refresh_token", utils.Params2Map("grant_type", "refresh_token", "refresh_token", refreshToken), "") + if err != nil { + return nil, err + } + return mapData2SNSToken(result), nil +} + +func (a *API) SNSGetUserInfo(accessToken, openid string) (*SNSUserInfo, error) { + result, err := a.AccessAPI("sns/userinfo", map[string]interface{}{ + "access_token": accessToken, + "openid": openid, + }, "") + if err == nil { + retVal := &SNSUserInfo{ + OpenID: utils.Interface2String(result["openid"]), + NickName: utils.Interface2String(result["nickname"]), + Sex: int(utils.MustInterface2Int64(result["sex"])), + Province: utils.Interface2String(result["province"]), + City: utils.Interface2String(result["city"]), + Country: utils.Interface2String(result["country"]), + HeadImgURL: utils.Interface2String(result["headimgurl"]), + Privilege: result["privilege"], + UnionID: utils.Interface2String(result["unionid"]), + } + return retVal, nil + } + return nil, err +} + +func (a *API) SNSIsOpenIDValid(accessToken, openid string) (bool, error) { + _, err := a.AccessAPI("sns/auth", map[string]interface{}{ + "access_token": accessToken, + "openid": openid, + }, "") + if err == nil { + return true, nil + } + return false, err +} + +func (a *API) SNSCode2Session(code string) (sessionInfo *SessionInfo, err error) { + result, err := a.AccessAPI("sns/jscode2session", map[string]interface{}{ + "js_code": code, + "grant_type": "authorization_code", + }, "") + if err == nil { + return &SessionInfo{ + OpenID: utils.Interface2String(result["openid"]), + SessionKey: utils.Interface2String(result["session_key"]), + UnionID: utils.Interface2String(result["unionid"]), + }, nil + } + return nil, err +} + +func (a *API) SNSDecodeMiniProgramData(encryptedData, sessionKey, iv string) (decryptedData []byte, err error) { + decodedDataList, err := utils.Base64DecodeMultiString(encryptedData, sessionKey, iv) + if err != nil { + return nil, err + } + c, err := aes.NewCipher(decodedDataList[1]) + if err != nil { + return nil, err + } + cfbdec := cipher.NewCFBDecrypter(c, decodedDataList[2]) + decryptedData = make([]byte, len(decodedDataList[0])) + cfbdec.XORKeyStream(decryptedData, decodedDataList[0]) + return decryptedData, nil +} diff --git a/platformapi/weixinsnsapi/weixinsnsapi.go b/platformapi/weixinsnsapi/weixinsnsapi.go deleted file mode 100644 index 6cac2b5e..00000000 --- a/platformapi/weixinsnsapi/weixinsnsapi.go +++ /dev/null @@ -1,207 +0,0 @@ -package weixinsnsapi - -import ( - "errors" - "net/http" - "strings" - "sync" - - "git.rosy.net.cn/baseapi/platformapi" - "git.rosy.net.cn/baseapi/utils" -) - -const ( - prodURL = "https://api.weixin.qq.com/sns" -) - -const ( - actionGetToken = "oauth2/access_token" - actionRefreshToken = "oauth2/refresh_token" -) - -const ( - ResponseCodeBusy = -1 - ResponseCodeSuccess = 0 -) - -type TokenInfo struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - OpenID string `json:"openid"` - Scope string `json:"scope"` -} - -type ErrorInfo struct { - ErrCode int `json:"errcode"` - ErrMsg string `json:"errmsg"` -} - -type API struct { - tokenInfo *TokenInfo - appID string - secret string - client *http.Client - config *platformapi.APIConfig - locker sync.RWMutex -} - -type UserInfo struct { - OpenID string `json:"openid"` - NickName string `json:"nickname"` - Sex int `json:"sex"` - Province string `json:"province"` - City string `json:"city"` - Country string `json:"country"` - HeadImgURL string `json:"headimgurl"` - Privilege interface{} `json:"privilege"` - UnionID string `json:"unionid"` -} - -var ( - ErrCodeAndRefreshTokenAllEmpty = errors.New("code and refresh are all empty") -) - -func New(appID, secret string, config ...*platformapi.APIConfig) *API { - curConfig := platformapi.DefAPIConfig - if len(config) > 0 { - curConfig = *config[0] - } - return &API{ - appID: appID, - secret: secret, - client: &http.Client{Timeout: curConfig.ClientTimeout}, - config: &curConfig, - } -} - -func (a *API) SetToken(newToken *TokenInfo) bool { - a.locker.Lock() - defer a.locker.Unlock() - a.tokenInfo = newToken - return true -} - -func (a *API) GetToken() *TokenInfo { - a.locker.RLock() - defer a.locker.RUnlock() - return a.tokenInfo -} - -func (a *API) AccessAPI(action string, params map[string]interface{}, body string) (retVal map[string]interface{}, err error) { - if params != nil && body != "" { - panic("params and body can not all non-empty") - } - params2 := make(map[string]interface{}) - for k, v := range params { - params2[k] = v - } - if action == actionGetToken || action == actionRefreshToken { - params2["appid"] = a.appID - params2["secret"] = a.secret - } else { - accessToken := a.GetToken() - if accessToken == nil { - panic("token is empty!") - } - params2["access_token"] = accessToken.AccessToken - // params2["openid"] = accessToken.OpenID - } - fullURL := utils.GenerateGetURL(prodURL, action, params2) - // baseapi.SugarLogger.Debug(fullURL) - - err = platformapi.AccessPlatformAPIWithRetry(a.client, - func() *http.Request { - var request *http.Request - if body == "" { - request, _ = http.NewRequest(http.MethodGet, fullURL, nil) - } else { - request, _ = http.NewRequest(http.MethodPost, fullURL, strings.NewReader(body)) - } - request.Close = true // todo try to fix EOF error when accessing weixin api. - return request - }, - a.config, - func(response *http.Response) (result string, err error) { - jsonResult1, err := utils.HTTPResponse2Json(response) - if err != nil { - return platformapi.ErrLevelGeneralFail, platformapi.ErrResponseDataFormatWrong - } - var errInfo *ErrorInfo - // 微信的返回值,在错误与正常情况下,结构是完全不一样的 - if errCode, ok := jsonResult1["errcode"]; ok { - errInfo = &ErrorInfo{ - ErrCode: int(utils.MustInterface2Int64(errCode)), - ErrMsg: jsonResult1["errmsg"].(string), - } - if errInfo.ErrCode == 0 { - retVal = jsonResult1 - } - } else { - retVal = jsonResult1 - } - if retVal != nil { - return platformapi.ErrLevelSuccess, nil - } - newErr := utils.NewErrorIntCode(errInfo.ErrMsg, errInfo.ErrCode) - if errInfo.ErrCode == ResponseCodeBusy { - return platformapi.ErrLevelRecoverableErr, newErr - } - return platformapi.ErrLevelCodeIsNotOK, newErr - }) - return retVal, err -} - -func (a *API) RefreshToken(code string) (tokenInfo *TokenInfo, err error) { - var result map[string]interface{} - if code != "" { - result, err = a.AccessAPI(actionGetToken, utils.Params2Map("grant_type", "authorization_code", "code", code), "") - } else { - token := a.GetToken() - if token != nil { - result, err = a.AccessAPI(actionRefreshToken, utils.Params2Map("grant_type", "refresh_token", "refresh_token", token.RefreshToken), "") - } else { - return nil, ErrCodeAndRefreshTokenAllEmpty - } - } - if err != nil { - return nil, err - } - tokenInfo = &TokenInfo{ - AccessToken: utils.Interface2String(result["access_token"]), - ExpiresIn: int(utils.MustInterface2Int64(result["expires_in"])), - RefreshToken: utils.Interface2String(result["refresh_token"]), - OpenID: utils.Interface2String(result["openid"]), - Scope: utils.Interface2String(result["scope"]), - } - // update my token too. - a.SetToken(tokenInfo) - return tokenInfo, nil -} - -func (a *API) GetUserInfo(openid string) (*UserInfo, error) { - result, err := a.AccessAPI("userinfo", utils.Params2Map("openid", openid), "") - if err == nil { - retVal := &UserInfo{ - OpenID: utils.Interface2String(result["openid"]), - NickName: utils.Interface2String(result["nickname"]), - Sex: int(utils.MustInterface2Int64(result["sex"])), - Province: utils.Interface2String(result["province"]), - City: utils.Interface2String(result["city"]), - Country: utils.Interface2String(result["country"]), - HeadImgURL: utils.Interface2String(result["headimgurl"]), - Privilege: result["privilege"], - UnionID: utils.Interface2String(result["unionid"]), - } - return retVal, nil - } - return nil, err -} - -func (a *API) IsOpenIDValid(openid string) (bool, error) { - _, err := a.AccessAPI("auth", utils.Params2Map("openid", openid), "") - if err == nil { - return true, nil - } - return false, err -} diff --git a/platformapi/weixinsnsapi/weixinsnsapi_test.go b/platformapi/weixinsnsapi/weixinsnsapi_test.go deleted file mode 100644 index e94148e8..00000000 --- a/platformapi/weixinsnsapi/weixinsnsapi_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package weixinsnsapi - -import ( - "testing" - - "git.rosy.net.cn/baseapi" - - "go.uber.org/zap" -) - -var ( - api *API - sugarLogger *zap.SugaredLogger -) - -func init() { - logger, _ := zap.NewDevelopment() - sugarLogger = logger.Sugar() - baseapi.Init(sugarLogger) - - // sandbox - api = New("wxbf235770edaabc5c", "ba32b269a068a5b72486a0beafd171e8") -} - -func TestRefreshToken(t *testing.T) { - result, err := api.RefreshToken("code") - if err != nil || result.ExpiresIn != 7200 { - t.Fatal(err.Error()) - } - sugarLogger.Debug(result) -} diff --git a/utils/utils.go b/utils/utils.go index bb8964bf..835bb098 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "encoding/base64" "fmt" "net/http" "net/url" @@ -224,3 +225,13 @@ func FilterMapNilMembers(mapData map[string]interface{}) map[string]interface{} } return mapData } + +func Base64DecodeMultiString(strs ...string) (decodedData [][]byte, err error) { + decodedData = make([][]byte, len(strs)) + for k, v := range strs { + if decodedData[k], err = base64.StdEncoding.DecodeString(v); err != nil { + return nil, err + } + } + return decodedData, nil +}