大多数小程序应用都会涉及到与用户身份相关数据以及功能,例如用户订单数据、用户支付功能,使用这些数据或功能前必须先获取用户身份标识并进行登录验证,微信小程序允许小程序应用使用登录授权机制来验证用户身份以及获取用户信息。微信小程序中使用OpenID与UnionID作为用户身份标识,在开始介绍用登录授权机制前先介绍一下OpenID与UnionID。
6.1小程序用户身份标识
OpenID是微信为每个公众号或者小程序的用户设置的身份识别的ID,简单来说,每个微信用户在每个公众号、小程序下的OpenID都不相同,这实际上是微信帮助每个公众号、小程序应用建立了一套天然的用户体系。若您的应用只有一个小程序,或者是存在多个小程序但是多个小程序相互之间不需要打通用户身份,那么直接使用OpenID就可以了。
如果开发者拥有多个移动应用、网站应用、和小程序应用(或者是公众号应用),可通过UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用、和小程序应用(或者是公众号应用),用户的UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID是相同的。要获取 UnionID,需要创建一个微信开放平台帐号,并将您的小程序绑定到微信开放平台帐号中。
6.2小程序登录流程
在讲述小程序登录相关的API前先介绍一下小程序登录的总体流程,下图是微信官方文档给出的小程序登录流程的时序图:
流程说明:
1)调用 wx.login() 获取临时登录凭证code ,并回传到开发者服务器。
2)在开发者服务器调用 auth.code2Session 接口,换取 用户唯一标识OpenID 、UnionID和会话密钥session_key。
3)开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
6.3获取微信登录凭证
调用 wx.login 获取临时登录凭证:code ,并回传到开发者服务器。这个code是用户打开小程序的时候微信平台生成的,每个code只能使用一次,因此,理论上这个code是安全的。
wx.login({
success: res => {
console.log(res.code);
//访问开发者服务器
}
});
wx.login这个API的作用就是为当前用户生成一个临时的登录凭证:code,这个临时code的有效期只有五分钟。我们拿到这个code后就可以进行下一步操作:把code发送发给业务服务器来获取登录session标识:
function userLogin(app, code) {
var url = 'http://127.0.0.1:80001/user_login';
wx.request({
url: url,
data: { code: code },
success: function (res) {
if (res.statusCode === 200) {
var ssid = res.data;
app.globalData.ssid = ssid;
}
}
});
}
获取到session标识后将session标识保存到小程序的全局的对象中,您也可以将session标识存储在本地缓存中,这样即使小程序退出了也可以在下次启动时恢复session标识,从而防止频繁调用wx.login。
凡是需要用到用户的身份标识的页面,都要进行未能登录并获取登录session标识。因此行将用户登录相关的逻辑安排在app.js文件是比较合理的,以下是增加了小程序登录逻辑后的app.js文件的内容:
function userLogin(app, code, cb) {
var domain = app.globalData.domain;
var url = domain + '/user_login';
wx.request({
url: url,
data: { code: code },
success: function (res) {
if (res.statusCode === 200) {
var ssid = res.data;
//保存session标识
app.setUserSsid(ssid);
//执行登录后的回调
if(cb != null) {
cb();
}
}
}
});
}
App({
onLaunch: function () {
//从Storage中加载session标识
var ssid = wx.getStorageSync('ssid');
if (ssid) {
this.globalData.ssid = ssid;
}
},
//获取登录标识
getUserSsid:function(){
var ssid = this.globalData.ssid;
var vars = ssid.split("-");
//登录是否超时
var time_now=new Date().getTime();
var stamp_now = parseInt(time_now/1000);
var stamp_lgoin = parseInt(vars[0]);
var idletm = stamp_now-stamp_lgoin;
if(idletm > 3600){
return ""
}
return ssid;
},
//保存登录标识
setUserSsid:function(ssid){
wx.setStorageSync('ssid', ssid);
this.globalData.ssid = ssid;
},
//小程序登录
wxLogin:function(cb){
var app = this;
wx.login({
success: res => {
//访问开发者服务器
userLogin(app, res.code, cb);
}
});
},
globalData: {
domain: "http://127.0.0.1:8001",
ssid:""
}
})
完成app.js的登录相关的代码后,您可以按照下面的方式为需要获取用户身份标识的页面增加登录检查逻辑:
index.js文件:
//访问Web服务器,获取motto的显示内容
function getWebData(page) {
var url = 'http://127.0.0.1:8001/hello';
wx.request({
url: url,
success: function (res) {
if (res.statusCode === 200) {
page.setData({ motto: res.data });
}
}
});
}
Page({
data: {
motto: '',
},
//登录成功后的调用
onLoginSuccess:function(){
getWebData(this);
},
onLoad: function (options) {
//若没有登录,则调用登录方法
var ssdata = getApp().getLogInfo();
if(ssdata.length < 1){
getApp().wxLogin(this.onLoginSuccess);
} else {
this.onLoginSuccess();
}
}
})
6.4 http.Client包的使用
可以使用net/http包来调用微信提供的服务端API。net/http包提供了最简洁的HTTP客户端实现,无需借助第三方网络通信库(比如libcurl)就可以直接发送GET和POST请求。
首先看看如何向第三方Web服务器发起HTTP GET请求,下面的代码实现了通过net/http包向服务器发送HTTP GET请求:
func HttpGet(url string) (string, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error:", err)
return "", err
}
if resp.StatusCode != 200 {
err := fmt.Errorf("status:%d", resp.StatusCode)
return string(result),err
}
return string(result), nil
}
在上面的代码中,我们创建可一个http.Client对象,并设置网络的超时时间是5秒,然后通过client.Get访问url指向的Web接口。接下来的代码实现了发送HTTP POST请求:
func HttpPost(url string, data []byte, dtype string) (string, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(url, dtype, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error:", err)
return "", err
}
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error:", err)
return "", err
}
if resp.StatusCode != 200 {
err := fmt.Errorf("status:%d", resp.StatusCode)
return string(result),err
}
return string(result), nil
}
有时需要在请求的时候设置请求头以及cookie参数,这时需要使用http.NewRequest方法以及Client.Do方法。首先调用http.NewRequest来创建一个请求对象,并使用请求对象设置自定义请求头信息。然后我们手需要动实例化Client对象,并传入添加了自定义请求头信息的请求对象,然后再调用Client.Do方法发起HTTP请求:
func HttpPostWidthHeader(url string, data []byte, dtype string, hdata map[string]string) (string, error) {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error:", err)
return "", err
}
req.Header.Set("Content-Type", dtype)
for k, v := range hdata{
req.Header.Set(k, v)
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error:", err)
return "", err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
err := fmt.Errorf("status:%d", resp.StatusCode)
return string(result),err
}
return string(result), nil
}
6.5小程序登录服务端实现
小程序获取到临时登录凭证后需要调用服务端接口才能获取用户身份标识。另外为了安全的考虑,服务端获取到用户身份标识后不能直接把OpenId返回给小程序,而是用OpenId生成一个Session标识,将Session标识返回给小程序。本节介绍OpenId的获取方法以及Session标识的生成逻辑。
6.5.1获取OpenId
服务端通过调用auth.code2Session接口来或取用户的OpenID,服务端调用auth.code2Session接口时要带上AppId和AppSecret。AppId和AppSecret是微信鉴别开发者身份的重要信息,AppId是公开信息,泄露AppId不会带来安全风险,但是AppSecret是开发者的隐私数据不应该泄露,如果发现有泄露需就要到小程序管理平台重置AppSecret。auth.code2Session接口的请求地址是:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
请求参数的定义如下:
属性 | 类型 | 说明 |
---|---|---|
appid | string | 小程序的appId |
secret | string | 小程序的appSecret |
js_code | string | 登录时获取的code |
grant_type | string | 授权类型,此处只需填写 authorization_code |
返回结果说明:
属性 | 类型 | 说明 |
---|---|---|
openid | string | 用户唯一标识 |
session_key | string | 会话密钥 |
unionid | string | 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回 |
errcode | number | 错误码 |
errmsg | string | 错误信息 |
以下是调用auth.code2Session接口获取用户的OpenID的服务端代码:
//根据code获取opendid以及session_key
func GetWxOpenIdByCode(appid string, secret string, code string) WxSessionData {
wx_addr := "https://api.weixin.qq.com/sns/jscode2session"
wx_addr = fmt.Sprintf("%s?appid=%s&secret=%s&js_code=%s&grant_type=%s",
wx_addr, appid, secret, code, "authorization_code")
var ent WxSessionData
data_str, err := HttpGet(wx_addr)
if err != nil {
log.Fatal("Error:", err)
return ent
}
err = json.Unmarshal([]byte(data_str), &ent)
if err != nil {
log.Fatal("Error:", err)
return ent
}
return ent
}
其中WxSessionData是微信接口的返回结果,定义如下:
type WxSessionData struct {
OpenId string `json:"openid"`
UnionId string `json:"unionid"`
Sessionkey string `json:"session_key"`
ErrCode int64 `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
6.5.2生成业务Session标识
用户身份验证成功后开发者服务器需为小程序客户端要生成Session标识,小程序后续发起的请求中需要携带Session标识,开发者服务器可以通过Session标识查询到当前登录用户的身份,这样我们就不需要每次都重新获取code,省去了很多网络消耗。
开发者要注意不应该直接把session_key、openid 等字段作为用户的标识或者session标识,而应该重新生成一个Session标识(请参考登录时序图)。对于开发者自己生成的Session标识,应该保证其安全性且不应该设置较长的过期时间。Session标识派发到小程序客户端之后,可将其存储在本地缓存中,用于后续通信使用。下面的代码是小程序登录后调用服务端获取Session标识服务端代码:
首先设置HTTP请求路由
mux.HandleFunc("/user_login", HandlerWxLogin)
然后定义是处理器函数的实现:
//登录微信后获取用户的openid
func HandlerWxLogin(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var code string=strings.Join(r.Form["code"],"")
if len(code) < 1 {
retmsng := "code不能为空!!!"
fmt.Println(retmsng)
w.WriteHeader(500)
w.Write([]byte(retmsng))
return
}
//获取openid以及Sessionkey
app_id:= "xxxx"//这里需要替换为您的小程序appid
app_secret:= "xxx"//这里需要替换为您小程序secret
wxtoken := GetWxOpenIdByCode(app_id, app_secret, code)
if len(wxtoken.OpenId) < 1 {
retmsng := "没有获取到OpenId"
fmt.Println(retmsng)
w.WriteHeader(500)
w.Write([]byte(retmsng))
return
}
var userid int64 = 0//这里需要替换为您业务内部的用户id
ssidstr := GenSessionString(userid, wxtoken.OpenId)
fmt.Println(ssidstr)
w.Write([]byte(ssidstr))
}
在代码中调用GetWxOpenIdByCode方法获取用户的OpenID,并调用GenSessionString来为小程序客户端生成业务Session标识。下面的代码GenSessionString函数的实现代码:
func GenSessionString(userid int64, openid string) string {
tmstr := fmt.Sprintf("%d", time.Now().Unix())
paramstr := fmt.Sprintf("%s-%d-%s", tmstr, userid, openid)
md5 := md5.Sum([]byte(paramstr))
md5str := fmt.Sprintf("%x", md5)
return fmt.Sprintf("%s-%d-%s", tmstr, userid, md5str)
}
注意:
1)上面的Session标识的生成机制比较简单,只是用于说明登录流程的逻辑,建议您编码时设计更为合理的Session标识的生成机制。
2)示例代码中将小程序的AppId和AppSecret“硬编码”到代码中,这不是一个良好的编码习惯,最好将AppId和AppSecret放到配置文件中。