github.com/fastwego/offiaccount@v1.0.1/apis/oauth/oauth.go (about)

     1  // Copyright 2020 FastWeGo
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package oauth 微信网页开发(oauth)
    16  
    17  /*
    18  网页授权流程分为四步:
    19  
    20  1、引导用户进入授权页面同意授权,获取code
    21  
    22  2、通过code换取网页授权access_token(与基础支持中的access_token不同)
    23  
    24  3、如果需要,开发者可以刷新网页授权access_token,避免过期
    25  
    26  4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)
    27  */
    28  package oauth
    29  
    30  import (
    31  	"encoding/json"
    32  	"fmt"
    33  	"io/ioutil"
    34  	"net/http"
    35  	"net/url"
    36  
    37  	"github.com/fastwego/offiaccount"
    38  )
    39  
    40  var OauthAuthorizeServerUrl = "https://open.weixin.qq.com"
    41  
    42  const (
    43  	apiAuthorize      = "/connect/oauth2/authorize"
    44  	apiAccessToken    = "/sns/oauth2/access_token"
    45  	apiRefreshToken   = "/sns/oauth2/refresh_token"
    46  	apiUserInfo       = "/sns/userinfo"
    47  	apiAuth           = "/sns/auth"
    48  	apiGetJSApiTicket = "/cgi-bin/ticket/getticket"
    49  )
    50  
    51  const (
    52  	ScopeSnsapiBase     = "snsapi_base"
    53  	ScopeSnsapiUserinfo = "snsapi_userinfo"
    54  )
    55  
    56  /*
    57  获取 用户授权 跳转链接
    58  
    59  以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
    60  
    61  以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息
    62  
    63  如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE
    64  
    65  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
    66  
    67  GET https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
    68  */
    69  func GetAuthorizeUrl(appid string, redirectUri string, scope string, state string) (authorizeUrl string) {
    70  	params := url.Values{}
    71  	params.Add("appid", appid)
    72  	params.Add("redirect_uri", redirectUri)
    73  	params.Add("response_type", "code")
    74  	params.Add("scope", scope)
    75  	params.Add("state", state)
    76  	return OauthAuthorizeServerUrl + apiAuthorize + "?" + params.Encode()
    77  }
    78  
    79  type OauthAccessToken struct {
    80  	AccessToken  string `json:"access_token"`
    81  	ExpiresIn    int    `json:"expires_in"`
    82  	RefreshToken string `json:"refresh_token"`
    83  	Openid       string `json:"openid"`
    84  	Scope        string `json:"scope"`
    85  }
    86  
    87  /*
    88  通过code换取网页授权access_token
    89  
    90  注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端
    91  
    92  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
    93  
    94  GET https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
    95  */
    96  func GetAccessToken(appid string, secret string, code string) (oauthAccessToken OauthAccessToken, err error) {
    97  	params := url.Values{}
    98  	params.Add("appid", appid)
    99  	params.Add("secret", secret)
   100  	params.Add("code", code)
   101  	params.Add("grant_type", "authorization_code")
   102  
   103  	uri := offiaccount.WXServerUrl + apiAccessToken + "?" + params.Encode()
   104  	response, err := http.Get(uri)
   105  	if err != nil {
   106  		return
   107  	}
   108  
   109  	defer response.Body.Close()
   110  	body, err := ioutil.ReadAll(response.Body)
   111  	if err != nil {
   112  		return
   113  	}
   114  
   115  	err = json.Unmarshal(body, &oauthAccessToken)
   116  	if err != nil {
   117  		err = fmt.Errorf("%s", string(body))
   118  		return
   119  	}
   120  
   121  	return
   122  }
   123  
   124  /*
   125  刷新access_token
   126  
   127  由于access_token拥有较短的有效期,当access_token超时后,可以使用refresh_token进行刷新,refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权
   128  
   129  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
   130  
   131  POST https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
   132  */
   133  func RefreshToken(appid string, refresh_token string) (oauthAccessToken OauthAccessToken, err error) {
   134  	params := url.Values{}
   135  	params.Add("appid", appid)
   136  	params.Add("refresh_token", refresh_token)
   137  	params.Add("grant_type", "refresh_token")
   138  
   139  	uri := offiaccount.WXServerUrl + apiRefreshToken + "?" + params.Encode()
   140  	response, err := http.Get(uri)
   141  	if err != nil {
   142  		return
   143  	}
   144  
   145  	defer response.Body.Close()
   146  	body, err := ioutil.ReadAll(response.Body)
   147  	if err != nil {
   148  		return
   149  	}
   150  
   151  	err = json.Unmarshal(body, &oauthAccessToken)
   152  	if err != nil {
   153  		err = fmt.Errorf("%s", string(body))
   154  		return
   155  	}
   156  
   157  	return
   158  }
   159  
   160  const (
   161  	LANG_zh_CN = "zh_CN"
   162  	LANG_zh_TW = "zh_TW"
   163  	LANG_en    = "en"
   164  )
   165  
   166  type OauthUserInfo struct {
   167  	Openid     string   `json:"openid"`
   168  	Nickname   string   `json:"nickname"`
   169  	Sex        int64    `json:"sex"`
   170  	Province   string   `json:"province"`
   171  	City       string   `json:"city"`
   172  	Country    string   `json:"country"`
   173  	Headimgurl string   `json:"headimgurl"`
   174  	Privilege  []string `json:"privilege"`
   175  	Unionid    string   `json:"unionid"`
   176  }
   177  
   178  /*
   179  拉取用户信息
   180  
   181  如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了
   182  
   183  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
   184  
   185  POST https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
   186  */
   187  func GetUserInfo(access_token string, openid string, lang string) (oauthUserInfo OauthUserInfo, err error) {
   188  	params := url.Values{}
   189  	params.Add("access_token", access_token)
   190  	params.Add("openid", openid)
   191  	params.Add("lang", lang)
   192  
   193  	uri := offiaccount.WXServerUrl + apiUserInfo + "?" + params.Encode()
   194  	response, err := http.Get(uri)
   195  	if err != nil {
   196  		return
   197  	}
   198  
   199  	defer response.Body.Close()
   200  	body, err := ioutil.ReadAll(response.Body)
   201  	if err != nil {
   202  		return
   203  	}
   204  
   205  	err = json.Unmarshal(body, &oauthUserInfo)
   206  	if err != nil {
   207  		err = fmt.Errorf("%s", string(body))
   208  		return
   209  	}
   210  
   211  	return
   212  }
   213  
   214  /*
   215  检验授权凭证(access_token)是否有效
   216  
   217  
   218  
   219  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
   220  
   221  GET https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID
   222  */
   223  func Auth(access_token string, openid string) (isValid bool, err error) {
   224  	params := url.Values{}
   225  	params.Add("access_token", access_token)
   226  	params.Add("openid", openid)
   227  
   228  	uri := offiaccount.WXServerUrl + apiAuth + "?" + params.Encode()
   229  	response, err := http.Get(uri)
   230  	if err != nil {
   231  		return
   232  	}
   233  
   234  	defer response.Body.Close()
   235  	body, err := ioutil.ReadAll(response.Body)
   236  	if err != nil {
   237  		return
   238  	}
   239  
   240  	s := struct {
   241  		Errcode int    `json:"errcode"`
   242  		Errmsg  string `json:"errmsg"`
   243  	}{}
   244  
   245  	err = json.Unmarshal(body, &s)
   246  	if err != nil {
   247  		err = fmt.Errorf("%s", string(body))
   248  		return
   249  	}
   250  
   251  	if s.Errcode == 0 {
   252  		isValid = true
   253  	}
   254  
   255  	return
   256  }
   257  
   258  /*
   259  获取 jsapi_ticket
   260  
   261  sapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket
   262  
   263  See: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
   264  
   265  GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
   266  */
   267  func GetJSApiTicket(ctx *offiaccount.OffiAccount) (jsapiTicket string, expiresIn int64, err error) {
   268  
   269  	jsapiTicketResp := struct {
   270  		Ticket    string `json:"ticket"`
   271  		ExpiresIn int64  `json:"expires_in"`
   272  	}{}
   273  	resp, err := ctx.Client.HTTPGet(apiGetJSApiTicket + "?type=jsapi")
   274  	if err != nil {
   275  		return
   276  	}
   277  
   278  	err = json.Unmarshal(resp, &jsapiTicketResp)
   279  	if err != nil {
   280  		return
   281  	}
   282  
   283  	return jsapiTicketResp.Ticket, jsapiTicketResp.ExpiresIn, nil
   284  }