有网友私信问什么时候出小程序制作视频教程,惦记很久一直迟迟没出,主要是因为最近感冒一直反反复复,精力不济,颇有一份欠债感,出视频思路还在构思备课中,课件做好了会逐步发出来,计划是先出一个入门级系列。出视频前先把一些点上的东西写写,也是个整理思路和经验的过程,希望和友善的友友们共同探讨和进步,本文只做思路和技术上的交流,脱离场景争论技术点的高低是没有任何意义的。
言归正传,这篇文章主要分析一下:微信小程序不泄露用户OpenId的前提下,用Go语言实现来实现用户静默登录生成Token的一种安全鉴权思路和具体实现方法。
一、场景:
做小程序基本上都会有会员注册的需求,但对于一个新的小程序,用户第一次打开就跳出让填写注册信息,如果不是那种非必须用的小程序,用户往往会直接关闭,把好不容易来的用户挡在了门外“吓”走了。那怎么实现不需要用户进行注册还能对用户信息进行唯一性识别呢?答案是静默获取用户OpenId,但如果获取与用户相关的信息直接用OpenId的话,很容易被扫描端口的恶意程序把敏感数据都拉跑,所以就需要既可以对用户进行唯一识别,还能防范被攻击的一种方法,本文主要讲利用Token来进行唯一性鉴权,当然有很多防范的方法,篇幅有限暂不做讨论。
二、技术路线
1、调用接口获取登录凭证(code)
由于满足一定的条件才能通过调用开放接口wx.login(Object object)获得登录凭证Code,且接口每次返回的Code每次不一样,所以扫描软件很难伪造出正确的code。
满足的条件见:https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/functional-pages/user-info.html
2、通过凭证换取用户登录态Token信息
在小程序端向服务器发起获取Token的请求,看到过一些项目在这一步是直接获取的openid,然后后续的接口都是通过openid作为参数来进行其他数据交互的,这么做的问题点是:用户openid直接对外暴露,虽然微信本身对每一个小程序生成的同一个用户openid都不一样,但对于单一小程序而言是永远不变的,恶意用户完全可以用不变的openid来持续的进行其他接口扫描,形成一定的数据泄露风险,如果某个接口再把其他用户的openid也暴露,那会产生更多的潜在数据外泄风险,所以本文采用的方法是返回token,而非openid,用户每次登录都会产生不一样的token,对恶意软件通过扫描接口来偷取数据就造成了很大的难度,如果token有效时间设置的比较短,安全性会更高。
前端登录请求全部代码片段如下:
login: function(customCallBack) {
var that = this;
uni.login({
success: function(res) {
if (res.code) { //wx.login获取code。
//发起网络请求
uni.request({
url: that.globalData.domain +'/admin/login',
data: {
code: res.code //将code发送到后台服务器。
},
dataType: 'json',
success(res1) {
if(res1.data.code==0){
//将token保存到全局变量
that.globalData.token=res1.data.data.token
//根据token获取用户的登录信息
//that.loadUserData(customCallBack);
}
if (getApp().successCallBack) { //回调函数,确保先得到token参数
getApp().successCallBack(getApp().globalData.token)
}
if (customCallBack) {
getApp().customCallBack = customCallBack
getApp().customCallBack(getApp().globalData.token)
}
}
})
}else {
console.log('获取用户登录态失败!' + res.errMsg)
}
},
complete:function(res){
console.log('登录',res)
}
})
},
后端登录接口admin/login实现方式使用了两个现成的轮子:
token工具包gtoken:github.com/goflyfox/gtoken/gtoken
微信接口工具包wechat:http://github.com/silenceper/wechat/v2
生成token代码:
package token
import (
"context"
"encoding/json"
"fmt"
v1 "ishejiao/api/v1"
"github.com/goflyfox/gtoken/gtoken"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
var (
// 启动gtoken,生成的token中有+号时,往回传递时需要将+替换为%2B
GfToken = >oken.GfToken{
LoginPath: "/login",
LoginBeforeFunc: login,
TokenDelimiter: "@", //Token分隔符,默认_
AuthFailMsg: "非法用户登录,拒绝访问", //认证失败提示
MultiLogin: true, //是否支持多端登录
CacheMode: 1,
LogoutPath: "/user/logout",
}
)
type tokenData struct {
UserKey string `json:"userKey"`
Uuid string `json:"uuid"`
}
func login(r *ghttp.Request) (string, interface{}) {
wechatUtil := v1.WechatUtil
ctx := r.Context()
code := g.RequestFromCtx(ctx).GetQuery("code").String()
fmt.Printf("====code=====: %s\n", code)
resCode2Session, err := v1.AuthWechat.Code2Session(code)
if err != nil {
r.Response.WriteJson(gtoken.Fail(err.Error()))
r.ExitAll()
}
// username := r.Get("username").String()
// passwd := r.Get("passwd").String()
openid := resCode2Session.OpenID
fmt.Printf("====openid=====: %s\n", openid)
//读取数据库进行判断,如果第一次登录可以将openid写入数据库,如果是老用户可以做一些其他逻辑判断
if openid == "" {
r.Response.WriteJson(gtoken.Fail("获取openid为空"))
r.ExitAll()
}
// 将openid藏到token中,以备后续其他接口解析使用,下边GetOpenid函数即为从token中获取解析到的openid,参数:唯一标识,扩展参数user data
return openid, openid
}
//从token中获取解析到的openid
func GetOpenid(ctx context.Context) string {
token := GfToken.DecryptToken(ctx, g.RequestFromCtx(ctx).GetQuery("token").String())
if token.Code != 0 {
return ""
}
var td tokenData
fmt.Println("---->tokenDataString:" + token.DataString())
jsonErr := json.Unmarshal([]byte(token.DataString()), &td)
if jsonErr != nil {
fmt.Println("jsonError:", jsonErr.Error())
}
fmt.Println("---->tokenJson:" + token.Json())
openid := td.UserKey
fmt.Println("---->解析出的openid:" + openid)
return openid
}
使用中间件进行token拦截
s := g.Server()
//认证接口
s.Group("/admin", func(group *ghttp.RouterGroup) {
//重点就是这一句!!!此处即为token中间件拦截
token.GfToken.Middleware(ctx, group)
group.Bind(
controller.Member, //获取用户信息
)
})
s.Run()
三、实例演示
下面以一个用到此方法的“爱社交”小程序(此示例小程序还在开发中)作为演示,先看一下界面。

该小程序最重要的一点就是用户隐私数据的安全性,如果用户名片信息被恶意软件扫描到进行批量拉取,那会造成大量用户数据泄露的风险,这显然是不可接受的,所以比起具体功能而言,数据安全性要求是第一位的。
为了保障用户数据安全性,采取的安全策略之一就是使用token来作为参数进行各项功能数据接口的读写。
下图为获取token的请求路径,参数code值即为wx.login获取到的code:

登录获取到的token数据返回数据为:

将获取到的token保存到全局变量即可为其他接口进行使用
that.globalData.token=res1.data.data.token
下图为用户获取自己名片信息的请求接口,只需要将token作为参数进行传递即可。

下图即为返回的部分数据内容

四、后记
在登录注册鉴权中很多种的实现方式,对于那些用户必须使用的常用软件,直接打开就是登录或者注册界面显然是有强势资源做支撑的,但对于大多数软件而言,如果用户不是很了解或者很必须,上来就先让一通注册登录的操作,基本上会吓退很大一批用户,所以循序渐进的递进获取用户信息是比较好的体验方式,用户打开软件只要能识别用户唯一身份即可,后续需要什么用户信息再逐步让用户填写,自然而然的会将用户信息逐步完善起来,比如电商小程序,用户购买物品的时候自然会输入个人信息,如果想小程序用户得到一个比较可观的积累,后续想做自己的APP,将引导到APP,也可以使用微信登录接口或者引导用户在小程序上设置用户名和密码即可。