feat: wechat provider
This commit is contained in:
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/pressly/goose/v3 v3.23.0
|
github.com/pressly/goose/v3 v3.23.0
|
||||||
github.com/samber/lo v1.47.0
|
github.com/samber/lo v1.47.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/smartystreets/goconvey v1.6.4
|
||||||
github.com/speps/go-hashids/v2 v2.0.1
|
github.com/speps/go-hashids/v2 v2.0.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/swaggo/fiber-swagger v1.3.0
|
github.com/swaggo/fiber-swagger v1.3.0
|
||||||
@@ -41,11 +42,13 @@ require (
|
|||||||
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
|
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
@@ -65,6 +68,7 @@ require (
|
|||||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.10.0 // indirect
|
github.com/spf13/afero v1.10.0 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
@@ -196,6 +197,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a h1:45JtCyuNYE+QN9aPuR1ID9++BQU+NMTMudHSuaK0Las=
|
github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a h1:45JtCyuNYE+QN9aPuR1ID9++BQU+NMTMudHSuaK0Las=
|
||||||
github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a/go.mod h1:RVHtZuvrpETIepiNUrNlih2OynoFf1eM6DGC6dloXzk=
|
github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a/go.mod h1:RVHtZuvrpETIepiNUrNlih2OynoFf1eM6DGC6dloXzk=
|
||||||
@@ -285,7 +287,9 @@ github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLy
|
|||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
|||||||
55
backend/providers/wechat/errors.go
Normal file
55
backend/providers/wechat/errors.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
// -1 系统繁忙,此时请开发者稍候再试
|
||||||
|
// 0 请求成功
|
||||||
|
// 40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
|
||||||
|
// 40002 请确保grant_type字段值为client_credential
|
||||||
|
// 40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。
|
||||||
|
// 40243 AppSecret已被冻结,请登录MP解冻后再次调用。
|
||||||
|
// 89503 此IP调用需要管理员确认,请联系管理员
|
||||||
|
// 89501 此IP正在等待管理员确认,请联系管理员
|
||||||
|
// 89506 24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用
|
||||||
|
// 89507 1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用
|
||||||
|
// 10003 redirect_uri域名与后台配置不一致
|
||||||
|
// 10004 此公众号被封禁
|
||||||
|
// 10005 此公众号并没有这些scope的权限
|
||||||
|
// 10006 必须关注此测试号
|
||||||
|
// 10009 操作太频繁了,请稍后重试
|
||||||
|
// 10010 scope不能为空
|
||||||
|
// 10011 redirect_uri不能为空
|
||||||
|
// 10012 appid不能为空
|
||||||
|
// 10013 state不能为空
|
||||||
|
// 10015 公众号未授权第三方平台,请检查授权状态
|
||||||
|
// 10016 不支持微信开放平台的Appid,请使用公众号Appid
|
||||||
|
func translateError(errCode int) error {
|
||||||
|
errors := map[int]error{
|
||||||
|
0: nil,
|
||||||
|
-1: errors.New("系统繁忙,此时请开发者稍候再试"),
|
||||||
|
40001: errors.New("AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性"),
|
||||||
|
40002: errors.New("请确保grant_type字段值为client_credential"),
|
||||||
|
40164: errors.New("调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置"),
|
||||||
|
40243: errors.New("AppSecret已被冻结,请登录MP解冻后再次调用"),
|
||||||
|
89503: errors.New("此IP调用需要管理员确认,请联系管理员"),
|
||||||
|
89501: errors.New("此IP正在等待管理员确认,请联系管理员"),
|
||||||
|
89506: errors.New("24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用"),
|
||||||
|
89507: errors.New("1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用"),
|
||||||
|
10003: errors.New("redirect_uri域名与后台配置不一致"),
|
||||||
|
10004: errors.New("此公众号被封禁"),
|
||||||
|
10005: errors.New("此公众号并没有这些scope的权限"),
|
||||||
|
10006: errors.New("必须关注此测试号"),
|
||||||
|
10009: errors.New("操作太频繁了,请稍后重试"),
|
||||||
|
10010: errors.New("scope不能为空"),
|
||||||
|
10011: errors.New("redirect_uri不能为空"),
|
||||||
|
10012: errors.New("appid不能为空"),
|
||||||
|
10013: errors.New("state不能为空"),
|
||||||
|
10015: errors.New("公众号未授权第三方平台,请检查授权状态"),
|
||||||
|
10016: errors.New("不支持微信开放平台的Appid,请使用公众号Appid"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err, ok := errors[errCode]; ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
14
backend/providers/wechat/funcs.go
Normal file
14
backend/providers/wechat/funcs.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
// RandomString generate random size string
|
||||||
|
func randomString(size int) (string, error) {
|
||||||
|
// generate size string [0-9a-zA-Z]
|
||||||
|
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
b := make([]byte, size)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = chars[rand.Intn(len(chars))]
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
69
backend/providers/wechat/options.go
Normal file
69
backend/providers/wechat/options.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options func(*Client)
|
||||||
|
|
||||||
|
func WithAppID(appID string) Options {
|
||||||
|
return func(we *Client) {
|
||||||
|
we.appID = appID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAppSecret sets the app secret
|
||||||
|
func WithAppSecret(appSecret string) Options {
|
||||||
|
return func(we *Client) {
|
||||||
|
we.appSecret = appSecret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithToken sets the token
|
||||||
|
func WithToken(token string) Options {
|
||||||
|
return func(we *Client) {
|
||||||
|
we.token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAESKey sets the AES key
|
||||||
|
func WithAESKey(aesKey string) Options {
|
||||||
|
return func(we *Client) {
|
||||||
|
we.aesKey = aesKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClient sets the http client
|
||||||
|
func WithClient(client *req.Client) Options {
|
||||||
|
return func(we *Client) {
|
||||||
|
we.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeAuthorizeURLOptions func(url.Values)
|
||||||
|
|
||||||
|
func ScopeAuthorizeURLWithScope(scope AuthScope) ScopeAuthorizeURLOptions {
|
||||||
|
return func(v url.Values) {
|
||||||
|
v.Set("scope", scope.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScopeAuthorizeURLWithRedirectURI(uri string) ScopeAuthorizeURLOptions {
|
||||||
|
return func(v url.Values) {
|
||||||
|
v.Set("redirect_uri", uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScopeAuthorizeURLWithState(state string) ScopeAuthorizeURLOptions {
|
||||||
|
return func(v url.Values) {
|
||||||
|
v.Set("state", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScopeAuthorizeURLWithForcePopup() ScopeAuthorizeURLOptions {
|
||||||
|
return func(v url.Values) {
|
||||||
|
v.Set("forcePopup", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
43
backend/providers/wechat/provider.go
Normal file
43
backend/providers/wechat/provider.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ipao.vip/rogeecn/atom/container"
|
||||||
|
"git.ipao.vip/rogeecn/atom/utils/opt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultProvider() container.ProviderContainer {
|
||||||
|
return container.ProviderContainer{
|
||||||
|
Provider: Provide,
|
||||||
|
Options: []opt.Option{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AppID string
|
||||||
|
AppSecret string
|
||||||
|
Token string
|
||||||
|
AesKey string
|
||||||
|
DevMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Provide(opts ...opt.Option) error {
|
||||||
|
o := opt.New(opts...)
|
||||||
|
var config Config
|
||||||
|
if err := o.UnmarshalConfig(&config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.Container.Provide(func() (*Client, error) {
|
||||||
|
httpClient := DefaultClient
|
||||||
|
if config.DevMode {
|
||||||
|
httpClient = httpClient.DevMode()
|
||||||
|
}
|
||||||
|
return New(
|
||||||
|
WithAppID(config.AppID),
|
||||||
|
WithAppSecret(config.AppSecret),
|
||||||
|
WithAESKey(config.AesKey),
|
||||||
|
WithToken(config.Token),
|
||||||
|
WithClient(httpClient),
|
||||||
|
), nil
|
||||||
|
}, o.DiOptions()...)
|
||||||
|
}
|
||||||
16
backend/providers/wechat/response.go
Normal file
16
backend/providers/wechat/response.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
ErrMsg int `json:"errmsg"`
|
||||||
|
ErrDescribe int `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Error() error {
|
||||||
|
return translateError(r.ErrCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"` // seconds
|
||||||
|
}
|
||||||
182
backend/providers/wechat/wechat.go
Normal file
182
backend/providers/wechat/wechat.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(options ...Options) *Client {
|
||||||
|
we := &Client{
|
||||||
|
client: DefaultClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(we)
|
||||||
|
}
|
||||||
|
|
||||||
|
return we
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (we *Client) GetAccessToken() (*AccessTokenResponse, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"grant_type": "client_credential",
|
||||||
|
}
|
||||||
|
|
||||||
|
var data AccessTokenResponse
|
||||||
|
_, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizeAccessToken struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
IsSnapshotuser int64 `json:"is_snapshotuser"`
|
||||||
|
Openid string `json:"openid"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Unionid string `json:"unionid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizeUserInfo struct {
|
||||||
|
City string `json:"city"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Headimgurl string `json:"headimgurl"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Openid string `json:"openid"`
|
||||||
|
Privilege []string `json:"privilege"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
Sex int64 `json:"sex"`
|
||||||
|
Unionid string `json:"unionid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
86
backend/providers/wechat/wechat_test.go
Normal file
86
backend/providers/wechat/wechat_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WechatAppID = "wx45745a8c51091ae0"
|
||||||
|
WechatAppSecret = "2ab33bc79d9b47efa4abef19d66e1977"
|
||||||
|
WechatToken = "W8Xhw5TivYBgY"
|
||||||
|
WechatAesKey = "F6AqCxAV4W1eCrY6llJ2zapphKK49CQN3RgtPDrjhnI"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient() *Client {
|
||||||
|
return New(
|
||||||
|
WithAppID(WechatAppID),
|
||||||
|
WithAppSecret(WechatAppSecret),
|
||||||
|
WithAESKey(WechatAesKey),
|
||||||
|
WithToken(WechatToken),
|
||||||
|
WithClient(DefaultClient.DevMode()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWechatClient_GetAccessToken(t *testing.T) {
|
||||||
|
Convey("Test GetAccessToken", t, func() {
|
||||||
|
token, err := getClient().GetAccessToken()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(token.AccessToken, ShouldNotBeEmpty)
|
||||||
|
So(token.ExpiresIn, ShouldBeGreaterThan, 0)
|
||||||
|
|
||||||
|
t.Log("Access Token:", token.AccessToken)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_ScopeAuthorizeURL(t *testing.T) {
|
||||||
|
Convey("Test ScopeAuthorizeURL", t, func() {
|
||||||
|
url, err := getClient().ScopeAuthorizeURL(
|
||||||
|
ScopeAuthorizeURLWithScope(ScopeBase),
|
||||||
|
ScopeAuthorizeURLWithRedirectURI("https://qvyun.mp.jdwan.com/"),
|
||||||
|
)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(url, ShouldNotBeEmpty)
|
||||||
|
t.Log("URL:", url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthorizeCode2Token(t *testing.T) {
|
||||||
|
code := "011W1sll2Xv4Ae4OjUnl2I7jvd2W1slX"
|
||||||
|
|
||||||
|
Convey("Test AuthorizeCode2Token", t, func() {
|
||||||
|
token, err := getClient().AuthorizeCode2Token(code)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
t.Logf("token: %+v", token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthorizeRefreshAccessToken(t *testing.T) {
|
||||||
|
token := "86_m_EAHq0RKlo6RzzGAsY8gVmiCqHqIiAJufxhm8mK8imyIW6yoE4NTcIr2vaukp7dexPWId0JWP1iZWYaLpXT_MJv1N7YQW8Qt3zOZDpJY90"
|
||||||
|
|
||||||
|
Convey("Test AuthorizeCode2Token", t, func() {
|
||||||
|
token, err := getClient().AuthorizeRefreshAccessToken(token)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
t.Logf("token: %+v", token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthorizeUserInfo(t *testing.T) {
|
||||||
|
token := "86_ZxJa8mIwbml5mDlHHbIUle_UKW8LA75nOuB0wqiome8AX5LlMWU8JwRKMZykdLEjDnKX8EJavz5GeQn3T1ot7TwpULp8imQvNIgFIjC4er8"
|
||||||
|
openID := "oMLa5tyJ2vRHa-HI4CMEkHztq3eU"
|
||||||
|
|
||||||
|
Convey("Test AuthorizeUserInfo", t, func() {
|
||||||
|
user, err := getClient().AuthorizeUserInfo(token, openID)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
t.Logf("user: %+v", user)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user