微信小程序一步一步獲取UnionID,實現(xiàn)自動登錄

思路:

1钉鸯、小程序端獲取用戶ID吧史,發(fā)送至后臺
2、后臺查詢用戶ID唠雕,如果找到了該用戶贸营,返回Token,沒找到該用戶岩睁,保存到數(shù)據(jù)庫钞脂,并返回Token

小程序端如何獲取用戶ID:

小程序端 wx.getUserInfo() 可以獲取到用戶信息

wx.getUserInfo()

其中 encryptedData 解密之后可以得到微信 UnionID,那么如何解密 encryptedData
解密 encryptedData

微信提供的解密 DEMO 包含4個版本:C++捕儒,Node芳肌,PHP,Python,Python需要安裝pycryptodome亿笤。
解密 encryptedData 需要 iv 和 session_key翎迁,獲取 session_key 需要訪問 auth.code2Session 接口
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 ,請登錄 微信開放平臺 => 管理中心 => 綁定小程序

源碼下載

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毁枯,一起剝皮案震驚了整個濱河市慈缔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌种玛,老刑警劉巖藐鹤,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赂韵,居然都是意外死亡娱节,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門右锨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碌秸,你說我怎么就攤上這事绍移。” “怎么了讥电?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵蹂窖,是天一觀的道長。 經(jīng)常有香客問我恩敌,道長瞬测,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮月趟,結(jié)果婚禮上灯蝴,老公的妹妹穿的比我還像新娘。我一直安慰自己孝宗,他們只是感情好穷躁,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著因妇,像睡著了一般问潭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婚被,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天狡忙,我揣著相機(jī)與錄音,去河邊找鬼址芯。 笑死灾茁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的是复。 我是一名探鬼主播删顶,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淑廊!你這毒婦竟也來了逗余?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤季惩,失蹤者是張志新(化名)和其女友劉穎录粱,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體画拾,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡啥繁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了青抛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旗闽。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蜜另,靈堂內(nèi)的尸體忽然破棺而出适室,到底是詐尸還是另有隱情,我是刑警寧澤举瑰,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布捣辆,位于F島的核電站,受9級特大地震影響此迅,放射性物質(zhì)發(fā)生泄漏汽畴。R本人自食惡果不足惜旧巾,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忍些。 院中可真熱鬧鲁猩,春花似錦、人聲如沸坐昙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炸客。三九已至疾棵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痹仙,已是汗流浹背是尔。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留开仰,地道東北人拟枚。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像众弓,于是被迫代替她去往敵國和親恩溅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容

  • unionId 一個微信開放平臺下的相同主體的App谓娃、公眾號脚乡、小程序的unionid是相同的,這樣就可以鎖定是不是...
    謝大見閱讀 63,640評論 1 27
  • 寫在前面 微信小程序出來也蠻久了滨达,經(jīng)過了市場的考驗奶稠,已經(jīng)站穩(wěn)腳跟,融入到了各行各業(yè)捡遍,市場需求激增打來的是開發(fā)人員的...
    月夢佳期閱讀 1,676評論 1 1
  • 一 前端 微信小程序以數(shù)據(jù)驅(qū)動的理念以及類jsx語法的形式锌订,以高集成高度封裝的方式開辟了H5新理念。 1.navi...
    PoWerfulHeart閱讀 10,447評論 0 1
  • 小程序面試題 小程序授權(quán)登錄流程 0画株、如何獲得用戶信息...
    X秀秀閱讀 1,806評論 0 8
  • 背景小程序一個比較重要的能力就是獲取用戶信息辆飘,也就是使用 wx.getUserInfo接口。我們發(fā)現(xiàn)幾乎所有的小程...
    未央大佬閱讀 15,437評論 0 23