diff --git a/consts.go b/consts.go index f433d61..bd7f65f 100644 --- a/consts.go +++ b/consts.go @@ -1,8 +1,15 @@ package main +// const ( +// WechatAppID = "wxf5bf0adeb99c2afd" +// WechatAppSecret = "3cf8fad4aa414f2b861399f111b22bb5" +// WechatToken = "W8Xhw5TivYBgY" +// WechatAesKey = "F6AqCxAV4W1eCrY6llJ2zapphKK49CQN3RgtPDrjhnI" +// ) + const ( - WechatAppID = "wxf5bf0adeb99c2afd" - WechatAppSecret = "3cf8fad4aa414f2b861399f111b22bb5" + WechatAppID = "wx45745a8c51091ae0" + WechatAppSecret = "2ab33bc79d9b47efa4abef19d66e1977" WechatToken = "W8Xhw5TivYBgY" WechatAesKey = "F6AqCxAV4W1eCrY6llJ2zapphKK49CQN3RgtPDrjhnI" ) diff --git a/main.go b/main.go index e69eebd..5e2f9cf 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,16 @@ package main import ( + "git.ipao.vip/rogeecn/mp-qvyun/pkg/middlewares/fiberv3" "git.ipao.vip/rogeecn/mp-qvyun/pkg/wechat" "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" ) +func init() { + log.SetLevel(log.DebugLevel) +} + func main() { wechatClient := wechat.New( wechat.WithAppID(WechatAppID), @@ -14,15 +19,17 @@ func main() { wechat.WithToken(WechatToken), ) + wechatMiddlewares := fiberv3.Init(wechatClient) + // create a new fiber server app := fiber.New() + app.Use(LogAll) + app.Use(wechatMiddlewares.Verify) + app.Use(wechatMiddlewares.AuthUserInfo) + app.Use(wechatMiddlewares.SilentAuth) - app.Use(VerifyWechatServer(wechatClient)) - - app.Get("/videos", func(c fiber.Ctx) error { - log.Infof("GET: %+v", c.Queries()) - log.Infof("POST: %s", c.Body()) - return c.SendString(c.Query("echostr", "Error")) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello World") }) // listen on port 3000 diff --git a/middlewares.go b/middlewares.go index 88e07e8..0aee68c 100644 --- a/middlewares.go +++ b/middlewares.go @@ -1,36 +1,17 @@ package main import ( - "git.ipao.vip/rogeecn/mp-qvyun/pkg/wechat" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" ) -func VerifyWechatServer(wechatClient *wechat.Client) fiber.Handler { - return func(c fiber.Ctx) error { - // get the query parameters - signature := c.Query("signature") - timestamp := c.Query("timestamp") - nonce := c.Query("nonce") - echostr := c.Query("echostr") - - if signature == "" || timestamp == "" || nonce == "" || echostr == "" { - return c.Next() - } - - log.Infof( - "begin verify signature, signature: %s, timestamp: %s, nonce: %s, echostr: %s", - signature, - timestamp, - nonce, - echostr, - ) - - // verify the signature - if err := wechatClient.VerifyServer(signature, timestamp, nonce); err != nil { - return c.SendString(err.Error()) - } - - return c.SendString(echostr) - } +func LogAll(c fiber.Ctx) error { + log.Info("------------------------------------------") + log.Infof("Request Method: %s", c.Method()) + log.Infof("Request Headers: %s", &c.Request().Header) + log.Infof("Request URL: %s", c.OriginalURL()) + log.Infof("Request Query: %+v", c.Queries()) + log.Infof("Request Body: %s", c.BodyRaw()) + log.Info("------------------------------------------") + return c.Next() } diff --git a/pkg/middlewares/fiberv3/middlewares.go b/pkg/middlewares/fiberv3/middlewares.go new file mode 100644 index 0000000..e05269f --- /dev/null +++ b/pkg/middlewares/fiberv3/middlewares.go @@ -0,0 +1,100 @@ +package fiberv3 + +import ( + "git.ipao.vip/rogeecn/mp-qvyun/pkg/wechat" + "github.com/gofiber/fiber/v3" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type fiberMiddlewares struct { + client *wechat.Client +} + +func Init(client *wechat.Client) *fiberMiddlewares { + return &fiberMiddlewares{ + client: client, + } +} + +func (f *fiberMiddlewares) Verify(c fiber.Ctx) error { + // get the query parameters + signature := c.Query("signature") + timestamp := c.Query("timestamp") + nonce := c.Query("nonce") + echostr := c.Query("echostr") + + if signature == "" || timestamp == "" || nonce == "" || echostr == "" { + return c.Next() + } + + log.Infof( + "begin verify signature, signature: %s, timestamp: %s, nonce: %s, echostr: %s", + signature, + timestamp, + nonce, + echostr, + ) + + // verify the signature + if err := f.client.Verify(signature, timestamp, nonce); err != nil { + return c.SendString(err.Error()) + } + + return c.SendString(echostr) +} + +func (f *fiberMiddlewares) SilentAuth(c fiber.Ctx) error { + // if cookie not exists key "openid", then redirect to the wechat auth page + sid := c.Cookies("sid", "") + if sid != "" { + // TODO: verify sid + return c.Next() + } + + // get current full url + url := c.BaseURL() + url = "https://qvyun.mp.jdwan.com" + log.WithField("module", "middleware.SilentAuth").Debug("url:", url) + + to, err := f.client.ScopeAuthorizeURL( + wechat.ScopeAuthorizeURLWithRedirectURI(url), + wechat.ScopeAuthorizeURLWithState("sns_basic_auth"), + ) + if err != nil { + return errors.Wrap(err, "failed to get wechat auth url") + } + log.WithField("module", "middleware.SilentAuth").Debug("redirectTo: ", to.String()) + + return c.Redirect().To(to.String()) +} + +func (f *fiberMiddlewares) AuthUserInfo(c fiber.Ctx) error { + state := c.Query("state") + code := c.Query("code") + + if state == "" && code == "" { + return c.Next() + } + + if state != "sns_basic_auth" { + return c.Next() + } + log.WithField("module", "middleware.AuthUserInfo").Debug("code", code) + + // get the openid + token, err := f.client.AuthorizeCode2Token(code) + if err != nil { + return errors.Wrap(err, "failed to get openid") + } + // TODO: store the openid to the session + + // set the openid to the cookie + c.Cookie(&fiber.Cookie{ + Name: "sid", + Value: token.Openid, + HTTPOnly: true, + }) + + return c.Redirect().To("/") +} diff --git a/pkg/wechat/errors.go b/pkg/wechat/errors.go index 1626f78..58c8609 100644 --- a/pkg/wechat/errors.go +++ b/pkg/wechat/errors.go @@ -12,6 +12,17 @@ import "github.com/pkg/errors" // 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, @@ -24,6 +35,17 @@ func translateError(errCode int) error { 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 { diff --git a/pkg/wechat/funcs.go b/pkg/wechat/funcs.go new file mode 100644 index 0000000..89a9699 --- /dev/null +++ b/pkg/wechat/funcs.go @@ -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 +} diff --git a/pkg/wechat/options.go b/pkg/wechat/options.go index 9acf503..fbf74eb 100644 --- a/pkg/wechat/options.go +++ b/pkg/wechat/options.go @@ -1,6 +1,10 @@ package wechat -import "github.com/imroc/req/v3" +import ( + "net/url" + + "github.com/imroc/req/v3" +) type Options func(*Client) @@ -37,3 +41,29 @@ func WithClient(client *req.Client) Options { 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") + } +} diff --git a/pkg/wechat/response.go b/pkg/wechat/response.go index fd13a4e..cea6324 100644 --- a/pkg/wechat/response.go +++ b/pkg/wechat/response.go @@ -12,5 +12,5 @@ func (r *Response) Error() error { type AccessTokenResponse struct { AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` + ExpiresIn int `json:"expires_in"` // seconds } diff --git a/pkg/wechat/wechat.go b/pkg/wechat/wechat.go index d5422fc..1874f62 100644 --- a/pkg/wechat/wechat.go +++ b/pkg/wechat/wechat.go @@ -3,6 +3,7 @@ package wechat import ( "crypto/sha1" "encoding/hex" + "net/url" "sort" "strings" @@ -17,6 +18,17 @@ var DefaultClient = req. 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 @@ -38,7 +50,7 @@ func New(options ...Options) *Client { return we } -func (we *Client) VerifyServer(signature, timestamp, nonce string) error { +func (we *Client) Verify(signature, timestamp, nonce string) error { params := []string{signature, timestamp, nonce, we.token} sort.Strings(params) str := strings.Join(params, "") @@ -52,11 +64,20 @@ func (we *Client) VerifyServer(signature, timestamp, nonce string) error { 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", - "appid": we.appID, - "secret": we.appSecret, } var data AccessTokenResponse @@ -67,3 +88,95 @@ func (we *Client) GetAccessToken() (*AccessTokenResponse, error) { 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 +} diff --git a/pkg/wechat/wechat_test.go b/pkg/wechat/wechat_test.go index 7c461f7..73afae4 100644 --- a/pkg/wechat/wechat_test.go +++ b/pkg/wechat/wechat_test.go @@ -8,27 +8,79 @@ import ( ) const ( - WechatAppID = "wxf5bf0adeb99c2afd" - WechatAppSecret = "3cf8fad4aa414f2b861399f111b22bb5" + 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() { - log.SetLevel(log.DebugLevel) - - wechatClient := New( - WithAppID(WechatAppID), - WithAppSecret(WechatAppSecret), - WithAESKey(WechatAesKey), - WithToken(WechatToken), - WithClient(DefaultClient.DevMode()), - ) - - token, err := wechatClient.GetAccessToken() + 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) }) }