package wechat import ( "crypto/sha1" "encoding/hex" "fmt" "net/url" "sort" "strings" "time" "quyun/v2/pkg/oauth" "github.com/imroc/req/v3" "github.com/pkg/errors" ) const BaseURL = "https://api.weixin.qq.com/" var DefaultClient = req. NewClient(). SetBaseURL(BaseURL). SetCommonHeader("Content-Type", "application/json") const ( ScopeBase = "snsapi_base" ScopeUserInfo = "snsapi_userinfo" ) type AuthScope string func (s AuthScope) String() string { return string(s) } type Client struct { client *req.Client appID string appSecret string token string aesKey string verifyKey string verifyValue string } func New(options ...Options) *Client { we := &Client{ client: DefaultClient, } for _, opt := range options { opt(we) } return we } func (we *Client) VerifySite(key string) (string, error) { if key == we.verifyKey { return we.verifyValue, nil } return "", errors.New("verify failed") } func (we *Client) Verify(signature, timestamp, nonce string) error { params := []string{signature, timestamp, nonce, we.token} sort.Strings(params) str := strings.Join(params, "") hash := sha1.Sum([]byte(str)) hashStr := hex.EncodeToString(hash[:]) if hashStr == signature { return errors.New("Signature verification failed") } return nil } func (we *Client) wrapParams(params map[string]string) map[string]string { if params == nil { params = make(map[string]string) } params["appid"] = we.appID params["secret"] = we.appSecret return params } // RefreshAccessToken func (we *Client) RefreshAccessToken(refreshToken string) (*AccessTokenResponse, error) { params := we.wrapParams(map[string]string{ "grant_type": "refresh_token", "refresh_token": refreshToken, }) var data AccessTokenResponse _, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token") if err != nil { return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed") } if data.ErrCode != 0 { return nil, data.Error() } return &data, nil } func (we *Client) GetAccessToken() (*AccessTokenResponse, error) { params := map[string]string{ "grant_type": "client_credential", } var data ErrorResponse resp, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/token") if err != nil { return nil, errors.Wrap(err, "call /cgi-bin/token failed") } if data.ErrCode != 0 { return nil, data.Error() } var token AccessTokenResponse if err := resp.Unmarshal(&token); err != nil { return nil, errors.Wrap(err, "parse response failed") } return &token, nil } // ScopeAuthorizeURL func (we *Client) ScopeAuthorizeURL(opts ...ScopeAuthorizeURLOptions) (*url.URL, error) { params := url.Values{} params.Add("appid", we.appID) params.Add("response_type", "code") for _, opt := range opts { opt(params) } if params.Get("scope") == "" { params.Add("scope", ScopeBase) } u, err := url.Parse("https://open.weixin.qq.com/connect/oauth2/authorize") if err != nil { return nil, errors.Wrap(err, "parse url failed") } u.Fragment = "wechat_redirect" u.RawQuery = url.Values(params).Encode() return u, nil } var _ oauth.OAuthInfo = (*AuthorizeAccessToken)(nil) type AuthorizeAccessToken struct { ErrorResponse AccessToken string `json:"access_token,omitempty"` ExpiresIn int64 `json:"expires_in,omitempty"` IsSnapshotuser int64 `json:"is_snapshotuser,omitempty"` Openid string `json:"openid,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` Scope string `json:"scope,omitempty"` Unionid string `json:"unionid,omitempty"` } // GetAccessToken implements oauth.OAuthInfo. func (a *AuthorizeAccessToken) GetAccessToken() string { return a.AccessToken } // GetExpiredAt implements oauth.OAuthInfo. func (a *AuthorizeAccessToken) GetExpiredAt() time.Time { return time.Now().Add(time.Duration(a.ExpiresIn) * time.Second) } // GetOpenID implements oauth.OAuthInfo. func (a *AuthorizeAccessToken) GetOpenID() string { return a.Openid } // GetRefreshToken implements oauth.OAuthInfo. func (a *AuthorizeAccessToken) GetRefreshToken() string { return a.RefreshToken } // GetUnionID implements oauth.OAuthInfo. func (a *AuthorizeAccessToken) GetUnionID() string { return a.Unionid } type StableAccessToken struct { AccessToken string `json:"access_token,omitempty"` ExpiresIn int64 `json:"expires_in,omitempty"` } func (we *Client) GetStableAccessToken() (*StableAccessToken, error) { params := we.wrapParams(map[string]string{ "grant_type": "client_credential", }) var data StableAccessToken _, err := we.client.R().SetSuccessResult(&data).SetBodyJsonMarshal(params).Post("/cgi-bin/stable_token") if err != nil { return nil, errors.Wrap(err, "call /cgi-bin/stable_token failed") } return &data, nil } func (we *Client) AuthorizeCode2Token(code string) (*AuthorizeAccessToken, error) { params := we.wrapParams(map[string]string{ "code": code, "grant_type": "authorization_code", }) var data AuthorizeAccessToken _, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/access_token") if err != nil { return nil, errors.Wrap(err, "call /sns/oauth2/access_token failed") } if err := data.Error(); err != nil { return nil, err } return &data, nil } func (we *Client) AuthorizeRefreshAccessToken(accessToken string) (*AuthorizeAccessToken, error) { params := we.wrapParams(map[string]string{ "refresh_token": accessToken, "grant_type": "refresh_token", }) var data AuthorizeAccessToken _, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token") if err != nil { return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed") } if err := data.Error(); err != nil { return nil, err } return &data, nil } type AuthorizeUserInfo struct { ErrorResponse City string `json:"city,omitempty"` Country string `json:"country,omitempty"` Headimgurl string `json:"headimgurl,omitempty"` Nickname string `json:"nickname,omitempty"` Openid string `json:"openid,omitempty"` Privilege []string `json:"privilege,omitempty"` Province string `json:"province,omitempty"` Sex int64 `json:"sex,omitempty"` Unionid string `json:"unionid,omitempty"` } func (we *Client) AuthorizeUserInfo(accessToken, openID string) (*AuthorizeUserInfo, error) { params := (map[string]string{ "access_token": accessToken, "openid": openID, }) var data AuthorizeUserInfo _, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/userinfo") if err != nil { return nil, errors.Wrap(err, "call /sns/userinfo failed") } if err := data.Error(); err != nil { return nil, err } return &data, nil } // GetJSTicket func (we *Client) GetJSTicket(token string) (string, error) { var data struct { Errcode int `json:"errcode"` Errmsg string `json:"errmsg"` Ticket string `json:"ticket"` ExpiresIn int `json:"expires_in"` } params := map[string]string{ "access_token": token, "type": "jsapi", } _, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/ticket/getticket") if err != nil { return "", errors.Wrap(err, "call /cgi-bin/ticket/getticket failed") } if data.Errcode != 0 { return "", errors.New("get wechat ticket failed: " + data.Errmsg) } return data.Ticket, nil } type JsSDK struct { Debug bool `json:"debug"` AppID string `json:"appId"` Timestamp int64 `json:"timestamp"` NonceStr string `json:"nonceStr"` Signature string `json:"signature"` } // GetJSTicket func (we *Client) GetJsSDK(token, url string) (*JsSDK, error) { sdk := &JsSDK{ Debug: false, AppID: we.appID, Timestamp: time.Now().Unix(), NonceStr: randomString(16), Signature: "", } // get ticket ticket, err := we.GetJSTicket(token) if err != nil { return nil, errors.Wrap(err, "get wechat ticket failed") } input := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticket, sdk.NonceStr, sdk.Timestamp, url) sdk.Signature = hashSha1(input) return sdk, nil }