思路:
1钉鸯、小程序端獲取用戶ID吧史,發(fā)送至后臺
2、后臺查詢用戶ID唠雕,如果找到了該用戶贸营,返回Token,沒找到該用戶岩睁,保存到數(shù)據(jù)庫钞脂,并返回Token
小程序端如何獲取用戶ID:
小程序端 wx.getUserInfo() 可以獲取到用戶信息
其中 encryptedData 解密之后可以得到微信 UnionID,那么如何解密 encryptedData
微信提供的解密 DEMO 包含4個版本:C++捕儒,Node芳肌,PHP,Python,Python需要安裝pycryptodome亿笤。
解密 encryptedData 需要 iv 和 session_key翎迁,獲取 session_key 需要訪問 auth.code2Session 接口
訪問 auth.code2Session 接口需要 appid 和 appSecret,直接保存在前端無疑是非常危險的净薛,正確的做法是:
1汪榔、小程序端調(diào)用 wx.login() 獲取 code,調(diào)用 wx.getUserInfo() 獲取 encryptedData 和 iv肃拜,發(fā)送 code痴腌、encryptedData 和 iv 到后臺,
2燃领、后臺訪問 auth.code2Session 接口士聪,獲取session_key, 使用 iv 和 session_key猛蔽,解密 encryptedData 獲取 UnionID剥悟,依據(jù) UnionID 查詢數(shù)據(jù)庫
注意:調(diào)用 wx.getUserInfo() 需要用戶授權(quán)
app.js
App({
data: {
canIUse: wx.canIUse('button.open-type.getUserInfo'), //版本兼容
serverHost: 'http://localhost:8090/',
token: null,
userInfo: null,
},
onLaunch: function() {
this.autoLogin();
},
//自動登錄
autoLogin: function() {
var that = this;
//查有沒有緩存 token, 緩存可能被清空
wx.getStorage({
key: 'token',
// 有token, 到后臺檢查 token 是否過期
success(res) {
console.log("token: " + res.data);
that.checkToken(res.data);
},
// 沒有緩存token, 需要登錄
fail(e) {
console.log("not saved token, login...");
that.userLogin();
}
})
},
//檢查 token 是否過期
checkToken: function(token) {
var that = this;
wx.request({
url: that.data.serverHost + 'user/token/check',
method: 'POST',
data: {
token: token,
},
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
success(res) {
if (res.data.code == 10000) {
console.log("token not expired");
} else {
console.log("token expired, refresh...");
// 去后臺刷新 token
that.refreshToken();
}
},
fail(e) {
console.error(e);
console.error("【check token failed, login...】");
// 走登錄流程
that.userLogin();
}
})
},
//刷新 token
refreshToken: function() {
var that = this;
//查有沒有緩存 refreshtoken, 緩存可能被清空
wx.getStorage({
key: 'refreshtoken',
// 有refreshtoken, 到后臺刷新 token
success(res) {
console.log("refreshtoken: " + res.data);
that.refreshToken2(res.data);
},
// 沒有緩存refreshtoken, 需要登錄
fail(e) {
console.log("not saved refreshtoken, login...");
that.userLogin();
}
})
},
//去后臺刷新 token
refreshToken2: function(refreshtoken) {
var that = this;
wx.request({
url: that.data.serverHost + 'user/token/refresh',
method: 'POST',
data: {
refreshtoken: refreshtoken,
},
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
success(res) {
if (res.data.code == 10000 && res.data.data.token) {
console.log(res.data.data.token);
that.saveToken(res.data.data.token)
} else {
console.log("refresh token failed, login...");
that.userLogin();
}
},
fail(e) {
console.error(e);
console.error("【refresh token failed, login...】");
that.userLogin();
}
})
},
// wx.login 獲取 code,
// wx.getUserInfo 獲取 encryptedData 和 iv
// 去后臺換取 token
userLogin: function() {
var that = this;
// wx.login 獲取 code,
wx.login({
success(res) {
if (res.code) {
console.log("code:" + res.code);
that.userLogin2(res.code);
} else {
console.error("【wx login failed】");
}
},
fail(e) {
console.error(e);
console.error("【wx login failed】");
}
})
},
// 檢查授權(quán), wx.getUserInfo
userLogin2: function(code) {
var that = this;
// 檢查是否授權(quán)
wx.getSetting({
success(res) {
// 已經(jīng)授權(quán), 可以直接調(diào)用 getUserInfo 獲取頭像昵稱
if (res.authSetting['scope.userInfo']) {
that.userLogin3(code);
} else { //沒有授權(quán)
if (that.data.canIUse) {
// 高版本, 需要轉(zhuǎn)到授權(quán)頁面
wx.navigateTo({
url: '/pages/auth/auth?code=' + code,
});
} else {
//低版本, 調(diào)用 getUserInfo, 系統(tǒng)自動彈出授權(quán)對話框
that.userLogin3(code);
}
}
}
})
},
// wx.getUserInfo
userLogin3: function(code) {
var that = this;
wx.getUserInfo({
success: function(res) {
console.log(res);
if (res.userInfo) {
that.data.userInfo = res.userInfo;
}
if (code && res.encryptedData && res.iv) {
that.userLogin4(code, res.encryptedData, res.iv);
} else {
console.error("【wx getUserInfo failed】");
}
},
fail(e) {
console.error(e);
console.error("【wx getUserInfo failed】");
}
})
},
//去后臺獲取用戶 token
userLogin4: function(code, data, iv) {
var that = this;
wx.request({
url: that.data.serverHost + 'user/wxlogin',
method: 'POST',
data: {
code: code,
data: data,
iv: iv,
},
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
success(res) {
console.log(res)
if (res.data.code == 10000) {
if (res.data.data.token) {
console.log(res.data.data.token);
that.saveToken(res.data.data.token);
} else {
console.error("【userLogin token failed】")
}
if (res.data.data.refreshtoken) {
console.log(res.data.data.refreshtoken);
wx.setStorage({
key: "refreshtoken",
data: res.data.data.refreshtoken
});
} else {
console.error("【userLogin refreshtoken failed】")
}
} else {
console.error("【userLogin failed】")
}
},
fail(e) {
console.error(e);
console.error("【userLogin failed】");
}
})
},
// 保存 token
saveToken: function(token) {
this.data.token = token;
wx.setStorage({
key: "token",
data: token
});
},
getUserInfo: function(call) {
var that = this
if (this.data.userInfo) {
call(this.data.userInfo);
} else {
// 先從緩存查 userInfo, 緩存可能被清空,
wx.getStorage({
key: 'userInfo',
success(res) {
console.log(res.data);
call(res.data);
that.setData({
userInfo: res.data
});
},
fail(res) {
console.log("not save userInfo, wx getUserInfo...");
wx.getUserInfo({
success(res) {
console.log(userInfo);
if (res.userInfo) {
call(res.userInfo);
that.setData({
userInfo: res.userInfo
});
}
}
})
}
})
}
},
})
auth.js
const app = getApp()
Page({
data: {
userInfo: {
avatarUrl: '/image/user_avarta.png',
nickName: '昵稱'
},
},
onLoad: function(param) {
this.data.code = param.code
},
getUserInfo: function(res) {
console.log(res.detail)
app.data.userInfo = res.detail.userInfo
this.setData({
userInfo: res.detail.userInfo,
})
if (this.data.code && res.detail.encryptedData && res.detail.iv) {
app.userLogin4(this.data.code, res.detail.encryptedData, res.detail.iv)
} else {
console.error("【getUserInfo失敗】");
}
}
})
授權(quán)頁面:auth.wxml
<view class="container">
<text class="prompt">授權(quán)登錄</text>
<view class="userinfo">
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</view>
<button open-type="getUserInfo" bindgetuserinfo="getUserInfo" type="primary"> 授權(quán)登錄 </button>
</view>
后端代碼
后端使用Python + Django 框架實現(xiàn):
安裝 requests ,發(fā)送Http請求
安裝 pycryptodome曼库,解密
pip install requests
pip install pycryptodome
此處僅給出View的代碼
import hashlib
import time
import json
import requests
from django.conf import settings
from django.http import JsonResponse
from django.views import View
from django_redis import get_redis_connection
from user.models import UserInfo
from utils.WXBizDataCrypt import WXBizDataCrypt
class WxLoginView(View):
def post(self, request):
post = request.POST
code = post.get('code')
if not code:
return JsonResponse({'code': 10001, 'msg': 'missing parameter: code'})
url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code" \
.format(settings.WX_APP_ID, settings.WX_APP_KEY, code)
# 發(fā)送GET請求
wx_res = requests.get(url)
errcode = wx_res['errcode'] if 'errcode' in wx_res else None
if errcode:
return JsonResponse({'code': 13001, 'msg': 'wx_auth.code2Session:' + wx_res.errmsg})
wx_session = json.loads(wx_res.text)
unionid = wx_session['unionId'] if 'unionId' in wx_session else None
decrypt = False
user = None
if not unionid:
decrypt = True
else:
user = UserInfo.objects.get(wx_unionid=unionid)
# 判斷用戶是否第一次登錄
if not user:
decrypt = True
# 解密 encryptedData
if decrypt:
encrypted_data = post.get('data')
iv = post.get('iv')
if not all([encrypted_data, iv]):
return JsonResponse({'code': 10001, 'msg': 'missing parameter: data,iv'})
session_key = wx_session['session_key'] if 'session_key' in wx_session else None
if not session_key:
return JsonResponse({'code': 13001, 'msg': 'wx_auth.code2Session:' + 'no session_key'})
pc = WXBizDataCrypt(settings.WX_APP_ID, session_key)
wx_user = pc.decrypt(encrypted_data, iv)
unionid = wx_user['unionId']
user = UserInfo.objects.get(wx_unionid=unionid)
# 判斷用戶是否第一次登錄
if not user:
# 微信用戶第一次登錄,創(chuàng)建用戶
username = 'wx_' + unionid
nickname = wx_user['nickName']
avatar = wx_user['avatarUrl']
gender = wx_user['gender']
country = wx_user['country']
province = wx_user['province']
city = wx_user['city']
language = wx_user['language']
user = UserInfo.objects.create(username=username,
wx_unionid=unionid,
nickname=nickname,
avatar=avatar,
gender=gender,
country=country,
province=province,
city=city,
language=language,
)
# 生成 token
md5 = hashlib.md5()
bstr = (unionid + str(time.time())).encode(encoding='utf-8')
md5.update(bstr)
token = md5.hexdigest()
bstr = ("refresh" + unionid + str(time.time())).encode(encoding='utf-8')
md5.update(bstr)
refreshtoken = md5.hexdigest()
# 存入Redis
conn = get_redis_connection('default')
conn.set(token, unionid)
conn.expire(token, 5)
conn.set(refreshtoken, unionid)
conn.expire(refreshtoken, 3600 * 24 * 7)
data = {'token': token, 'expire': 3600, 'refreshtoken': refreshtoken}
return JsonResponse({'code': 10000, 'msg': 'ok', 'data': data})
class TokenCheckView(View):
def post(self, request):
post = request.POST
token = post.get('token')
if not token:
return JsonResponse({'code': 10001, 'msg': 'missing parameter: token'})
conn = get_redis_connection('default')
exist = conn.ttl(token)
if exist < 0:
return JsonResponse({'code': 10200, 'msg': 'token expired'})
else:
return JsonResponse({'code': 10000, 'msg': 'ok'})
class TokenRefreshView(View):
def post(self, request):
post = request.POST
refreshtoken = post.get('refreshtoken')
if not refreshtoken:
return JsonResponse({'code': 10001, 'msg': 'missing parameter: refreshtoken'})
conn = get_redis_connection('default')
unionid = conn.get(refreshtoken)
if not unionid:
return JsonResponse({'code': 10200, 'msg': 'refreshtoken expired'})
# 生成 token
md5 = hashlib.md5()
bstr = unionid + str(time.time()).encode(encoding='utf-8')
md5.update(bstr)
token = md5.hexdigest()
conn.set(token, unionid)
conn.expire(token, 5)
data = {'token': token}
return JsonResponse({'code': 10000, 'msg': 'ok', 'data': data})
注意:
如果解壓之后区岗,沒有獲取到 UnionID ,請登錄 微信開放平臺 => 管理中心 => 綁定小程序