django + jwt 完成微信小程序身份驗證,步驟如下
環境說明:
1、小程序只需要拿到openid,其他信息不存儲。
2、Django自帶的User類不適合。需要對django user 進行擴展
流程
1.使用微信小程序登錄和獲取用戶信息Api接口
2.把Api獲取的用戶資料和code發送給django后端
3.通過微信接口把code換取成openid
4.后端將openid作為用戶名和密碼
5.后端通過JSON web token方式登錄,把token和用戶id傳回小程序
6.小程序將token和用戶id保存在storage中
7.下次請求需要驗證用戶身份的頁面時,在請求中加入token這個字段
微信小程序前端框架使用iview-weapp
https://github.com/TalkingData/iview-weapp
第一步注冊微信小程序
第二步下載微信開發工具
http://www.ionic.wang/weixin/devtools/download.html
第三步新建小程序項目
項目結構:
修改app.js
新增小程序登錄頁面
打開項目pages目錄-->login 文件夾
修改login.js login.json login.wxml login.wxss
login.js 代碼如下
// pages/login/login.js const app = getApp() const { $Toast } = require('../../dist/base/index') const { $Message } = require('../../dist/base/index'); Page({ /** * 頁面的初始數據 */ data: { yhxyVisible: false, ystkVisible: false }, /** * 生命周期函數--監聽頁面加載 */ onLoad: function (options) { wx.getUserInfo({ success: function (res) { var userInfo = res.userInfo var nickName = userInfo.nickName console.log(nickName) app.globalData.userInfo.nickName = nickName var avatarUrl = userInfo.avatarUrl app.globalData.userInfo.avatarUrl = avatarUrl var gender = userInfo.gender //性別 0:未知、1:男、2:女 app.globalData.userInfo.gender = gender var province = userInfo.province var city = userInfo.city var country = userInfo.country } }) }, /** * 生命周期函數--監聽頁面初次渲染完成 */ onReady: function () { }, /** * 生命周期函數--監聽頁面顯示 */ onShow: function () { }, /** * 生命周期函數--監聽頁面隱藏 */ onHide: function () { }, /** * 生命周期函數--監聽頁面卸載 */ onUnload: function () { }, /** * 頁面相關事件處理函數--監聽用戶下拉動作 */ onPullDownRefresh: function () { }, /** * 頁面上拉觸底事件的處理函數 */ onReachBottom: function () { }, /** * 用戶點擊右上角分享 */ onShareAppMessage: function () { }, //手機號登錄 mobileLogin(e) { $Toast({ content: '登錄中...', type: 'loading', duration: 0, mask: false }); console.log(e.detail.errMsg) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.login({ success: res => { console.log(res) //請求后端換取openid的接口 wx.request({ url: 'http://199394.wezoz.com/landlorde/app/mobileLogin/', method: 'POST', data: { //將code和用戶基礎信息傳到后端 jscode: res.code, iv: e.detail.iv, encryptedData: e.detail.encryptedData, nickname: app.globalData.userInfo.nickName, avatar_url: app.globalData.userInfo.avatarUrl, gender: app.globalData.userInfo.gender }, success: res => { //獲取到openid作為賬號 console.log("res=============", res) console.log(app.globalData.userInfo) if (res.data.code == 200 && res.data.msg == "ok") { //this.reFreshUserProfile() wx.setStorageSync('token', res.data.token) app.globalData.isLogin = true app.globalData.hasUserInfo = true app.globalData.token = res.data.token $Toast.hide(); wx.redirectTo({ url: '../index/index?id=1' }) } else { wx.showToast({ title: '網絡發生錯誤請稍后再試!', icon: 'error', duration: 2000 }) } } }) } }) }, //微信登錄 wxlogin() { $Toast({ content: '登錄中...', type: 'loading', duration: 0, mask: false }); wx.login({ success: res => { console.log(res) //請求后端換取openid的接口 wx.request({ url: 'http://199394.wezoz.com/landlorde/app/wxlogin/', method: 'POST', data: { //將code傳到后端 jscode: res.code }, success: res => { //獲取到openid作為賬號 console.log("res=============", res) console.log(app.globalData.userInfo) if (res.data.code == 200 && res.data.msg == "ok") { //this.reFreshUserProfile() wx.setStorageSync('token', res.data.token) app.globalData.isLogin = true app.globalData.hasUserInfo = true app.globalData.token = res.data.token $Toast.hide(); wx.redirectTo({ url: '../index/index?id=1' }) } else { wx.showToast({ title: '登錄失敗請稍后再試!', icon: 'error', duration: 2000 }) } } }) } }) }, yhxy() { this.setData({ yhxyVisible: true }); }, yhxyok() { this.setData({ yhxyVisible: false }); }, yhxycancel() { this.setData({ yhxyVisible: false }); }, ystk() { this.setData({ ystkVisible: true }); }, ystkok() { this.setData({ ystkVisible: false }); }, ystkcancel() { this.setData({ ystkVisible: false }); }, })
login.json
{ "navigationBarTitleText": "登錄", "usingComponents": { "i-button": "../../dist/button/index", "i-panel": "../../dist/panel/index", "i-avatar": "../../dist/avatar/index", "i-row": "../../dist/row/index", "i-col": "../../dist/col/index", "i-toast": "../../dist/toast/index", "i-modal": "../../dist/modal/index", "i-message": "../../dist/message/index" } }
login.wxml
<!--pages/login/login.wxml--> <view class="container"> <image class="logStyle" src="http://img11.360buyimg.com/mobilecms/s700x256_jfs/t20539/229/82605291/66559/df347eb5/5af96a18N9451b1a1.jpg"></image> <i-button bindgetphonenumber="mobileLogin" style="background: #ff6700;" class="btnLoginStyle" open-type="getPhoneNumber" type="warning" shape="circle" size="large">手機快捷登錄</i-button> <text class="weChatLogin" bindtap="wxlogin">微信登錄</text> </view> <view class="company"> <view>登錄/注冊代表您已閱讀並同意 <text class="descStyle" bindtap="yhxy">用戶協議</text>和 <text class="descStyle" bindtap="ystk">隱私條款</text> </view> </view> <i-toast id="toast" /> <i-modal title="用戶協議" visible="{{ yhxyVisible }}" bind:ok="yhxyok" bind:cancel="yhxycancel"> <view>1.用戶協議用戶協議用戶協議</view> <view>2.用戶協議用戶協議用戶協議</view> <view>3.用戶協議用戶協議用戶協議</view> </i-modal> <i-modal title="隱私條款" visible="{{ ystkVisible }}" bind:ok="ystkok" bind:cancel="ystkcancel"> <view>1.隱私條款隱私條款隱私條款</view> <view>2.隱私條款隱私條款隱私條款</view> <view>3.隱私條款隱私條款隱私條款</view> </i-modal>
login.wxss
/* pages/login/login.wxss */ .item { height: 100px; text-align: center; } .container { display: block; width: 200px; height: 100px; margin: 0 auto; position: relative; text-align: center; } .logStyle { width: 100px; height: 100px; background-color: #eeeeee; margin-top: 90rpx; margin-bottom: 50rpx; border-radius: 50%; } .i-btn-warning{ background: #ff6700 !important; } .descStyle { color: #ff6700; } .company { position: absolute; bottom: 30rpx; width: 100%; display: flex; justify-content: center; font-size: 12px; } .weChatLogin{ font-size: 14px; }
第四步新建django項目
可參照微信開發入門篇一 https://www.cnblogs.com/wangcongxing/p/11546780.html
第五步處理微信登錄
打開django項目,結構如下
authentication.py
from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication from rest_framework_jwt.settings import api_settings class UserAuthentication(BaseAuthentication): def authenticate(self, request): if 'token' in request.data: try: token = request.data['token'] jwt_decode_handler = api_settings.JWT_DECODE_HANDLER user_dict = jwt_decode_handler(token) return (user_dict, token) except Exception as ex: raise exceptions.AuthenticationFailed(detail={'code': 401, 'msg': 'skey已過期'}) else: raise exceptions.AuthenticationFailed(detail={'code': 400, 'msg': '缺少token'}) def authenticate_header(self, request): return 'skey'
WXBizDataCrypt.py
import base64 import json from Crypto.Cipher import AES class WXBizDataCrypt: def __init__(self, appId, sessionKey): self.appId = appId self.sessionKey = sessionKey def decrypt(self, encryptedData, iv): # base64 decode sessionKey = base64.b64decode(self.sessionKey) encryptedData = base64.b64decode(encryptedData) iv = base64.b64decode(iv) cipher = AES.new(sessionKey, AES.MODE_CBC, iv) decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData))) if decrypted['watermark']['appid'] != self.appId: raise Exception('Invalid Buffer') return decrypted def _unpad(self, s): return s[:-ord(s[len(s)-1:])]
urls.py
from django.contrib import admin from django.urls import path from django.urls import path, include from app import views from app import test_view urlpatterns = [ # 微信小程序登錄 path('wxlogin/', views.code2Session), # 微信手機號碼登錄 path('mobileLogin/', views.mobileLogin), # 通過token獲取用轉換用戶信息 path('checkToken/', views.checkToken), ]
models.py
from django.db import models # Create your models here. from django.contrib.auth.models import AbstractUser from django.db import models class NewUser(AbstractUser): nickname = models.CharField(max_length=225, verbose_name="昵稱", default="") avatar_url = models.CharField(max_length=225, verbose_name="頭像", default="") gender = models.CharField(max_length=225, verbose_name="性別", default="") session_key = models.CharField(max_length=225, verbose_name="session_key", default="") mobilePhoneNumber = models.CharField(max_length=225, verbose_name="手機號碼", default="")
views.py
from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt # Create your views here. from django.http import HttpResponse from django.contrib.auth.models import Permission, User from django.contrib import auth from django.views.decorators.http import require_http_methods from rest_framework.views import APIView from rest_framework_jwt.settings import api_settings from django.core import serializers from django.http import JsonResponse, HttpResponse, HttpResponseRedirect from django.shortcuts import render, redirect import uuid, datetime, json, time from datetime import timedelta from dateutil.relativedelta import relativedelta from wechatpy import WeChatClient import json, re, urllib3 import os import json from wechatpy import WeChatPay from django.views.decorators.csrf import csrf_exempt import time import random from django.core.cache import cache # 引入緩存模塊 from wechatpy.utils import timezone from django.db import transaction from django import forms from django.db.models import F import requests from django.contrib.auth.models import Permission, User from django.db import connection from django.db.models import Sum @csrf_exempt def login(request): if request.method == "POST": username = request.POST.get('username') passwd = request.POST.get('passwd') user = User.objects.filter(username=username).first() auth.login(request, user) # 登錄成功 print(username) print(passwd) return HttpResponse("登錄成功!") else: return HttpResponse("請求錯誤") #小程序appid appid = "wxxxxxxxxx" # 小程序秘鑰 appsecret = "xxxxxxxxxxxx" ''' 登錄函數: ''' @require_http_methods(['POST']) @csrf_exempt def GetOpenIdView(request): data = json.loads(request.body) jscode = data['jscode'] openid, session_key = OpenId(jscode).get_openid() return JsonResponse({ 'openid': openid, 'session_key': session_key }) from app import models @require_http_methods(['POST']) @csrf_exempt def login_or_create_account(request): data = json.loads(request.body) print(data) openid = data['openid'] nickname = data['nickname'] avatar_url = data['avatar_url'] gender = data['gender'] try: user = models.NewUser.objects.get(username=openid) except models.NewUser.DoesNotExist: user = None if user: user = models.NewUser.objects.get(username=openid) else: user = models.NewUser.objects.create( username=openid, password=openid, nickname=nickname, avatar_url=avatar_url, gender=gender ) jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) res = { 'status': 'success', 'nickname': user.nickname, 'user_id': user.id, 'token': token } return JsonResponse(res) class OpenId: def __init__(self, jscode): self.url = 'https://api.weixin.qq.com/sns/jscode2session' self.app_id = appid self.app_secret = appsecret self.jscode = jscode def get_openid(self): url = self.url + "?appid=" + self.app_id + "&secret=" + self.app_secret + "&js_code=" + self.jscode + "&grant_type=authorization_code" res = requests.get(url) try: openid = res.json()['openid'] session_key = res.json()['session_key'] except KeyError: return 'fail' else: return openid, session_key import hashlib import json import requests from rest_framework import status from rest_framework.decorators import api_view, authentication_classes from rest_framework.response import Response from django_redis import get_redis_connection from app.WXBizDataCrypt import WXBizDataCrypt appid = "" secret = "" @api_view(['POST']) @authentication_classes([]) # 添加 def code2Session(request): js_code = request.data['jscode'] url = 'https://api.weixin.qq.com/sns/jscode2session' + '?appid=' + appid + '&secret=' + secret + '&js_code=' + js_code + '&grant_type=authorization_code' response = json.loads(requests.get(url).content) # 將json數據包轉成字典 if 'errcode' in response: # 有錯誤碼 return Response(data={'code': response['errcode'], 'msg': response['errmsg']}) # 登錄成功 openid = response['openid'] session_key = response['session_key'] # 保存openid, 需要先判斷數據庫中有沒有這個openid user = models.NewUser.objects.filter(username=openid).first() if user is None: user = models.NewUser.objects.create( username=openid, password=uuid.uuid4(), session_key=session_key ) else: user.session_key = session_key user.save() # 將自定義登錄態保存到緩存中, 兩個小時過期 jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) skey = jwt_encode_handler(payload) print(skey) return JsonResponse({'code': 200, 'msg': 'ok', 'token': skey}) def filter_emoji(desstr, restr=''): # 過濾表情 try: res = re.compile(u'[\U00010000-\U0010ffff]') except re.error: res = re.compile(u'[\uD800-\uDBFF][\uDC00-\uDFFF]') return res.sub(restr, desstr) @api_view(['POST']) @authentication_classes([]) # 添加 def mobileLogin(request): js_code = request.data['jscode'] iv = request.data["iv"] encryptedData = request.data["encryptedData"] nickname = request.data["nickname"] avatar_url = request.data["avatar_url"] gender = request.data["gender"] nickname = filter_emoji(nickname, '') if js_code is None or iv is None or encryptedData is None or avatar_url is None: return JsonResponse({'code': 400, 'msg': '系統維護,請稍后再試!'}) url = 'https://api.weixin.qq.com/sns/jscode2session' + '?appid=' + appid + '&secret=' + secret + '&js_code=' + js_code + '&grant_type=authorization_code' response = json.loads(requests.get(url).content) # 將json數據包轉成字典 if 'errcode' in response: # 有錯誤碼 return JsonResponse({'code': response['errcode'], 'msg': response['errmsg']}) try: openid = response['openid'] session_key = response['session_key'] wxdc = WXBizDataCrypt(appid, session_key) pResult = wxdc.decrypt(encryptedData, iv) print(pResult) # 保存openid, 需要先判斷數據庫中有沒有這個openid user = models.NewUser.objects.filter(username=openid).first() if user is None: user = models.NewUser.objects.create( username=openid, password=uuid.uuid4(), session_key=session_key, nickname=nickname, avatar_url=avatar_url, gender=gender, mobilePhoneNumber=pResult["phoneNumber"] ) else: user.session_key = session_key user.nickname = nickname, user.avatar_url = avatar_url, user.gender = gender user.save() # token有效期1天,settings.py 文件中設置 jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) print(token) return JsonResponse({'code': 200, 'msg': 'ok', 'token': token}) except Exception as ex: return JsonResponse({'code': 400, 'msg': "發生錯誤請稍后再試!"}) from app import authentication @api_view(['POST']) @authentication_classes([authentication.UserAuthentication]) # 添加 def checkToken(request): print(request.user) print(request.user["username"]) return JsonResponse({'code': 200, 'msg': 'ok', 'userInfo': request.user})
settings.py
""" Django settings for landlorde project. Generated by 'django-admin startproject' using Django 3.0.5. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '1mf87-kx7x6*zoio^f6@8oqu*t=**pmv*i^kduz*hc)iw4r(5q' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app.apps.AppConfig', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'landlorde.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'landlorde.wsgi.application' # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'HOST': '127.0.0.1', 'PORT': '3306', 'NAME': 'landlorde', 'USER': 'root', 'PASSWORD': '123456' } } CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379', "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, }, } REDIS_TIMEOUT = 7 * 24 * 60 * 60 CUBES_REDIS_TIMEOUT = 60 * 60 NEVER_REDIS_TIMEOUT = 365 * 24 * 60 * 60 # 開發redis 路徑 C:\Program Files\Redis redis-server redis.windows.conf ''' windows下安裝Redis第一次啟動報錯: [2368] 21 Apr 02:57:05.611 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error 解決方法:在命令行中運行 redis-cli.exe 127.0.0.1:6379>shutdown not connected>exit 然后重新運行redis-server.exe redis.windows.conf ''' ''' 測試地址 http://199394.wezoz.com/landlorde/app/get-openid/ http://199394.wezoz.com/landlorde/app/wx-login/ ''' # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/landlordestatic/' STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'landlordestatic'), ) # SIMPLEUI 配置 SIMPLEUI_STATIC_OFFLINE = True SIMPLEUI_HOME_INFO = False # 圖片上傳路徑 MEDIA_URL = '/' MEDIA_ROOT = r'D:\landlordestatic' # 服務號配置 domain = "http://www.xxxx.com/" keysPath = os.path.join(BASE_DIR, 'keys') AUTH_USER_MODEL = "app.NewUser" weChatConfig = { 'appid': "wxxxxxxxxxx", 'appsecret': "xxxxxxxxxxxxxxx", 'mch_id': "xxxxxxxx", 'notify_url': domain + '/notify_url/', 'baiduapiak': 'xxxxxxxxxxxxxxx', 'baiduapiwebak': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'mch_cert': os.path.join(keysPath, r"apiclient_cert.pem"), 'mch_key': os.path.join(keysPath, r"apiclient_key.pem"), } # 在末尾添加上 import datetime # 在末尾添加上 ''' REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',# JWT認證,在前面的認證方案優先 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), } ''' JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), #JWT_EXPIRATION_DELTA 指明token的有效期 } REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'app.authentication.UserAuthentication', # 用自定義的認證類 ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', ), }
requirements.txt
amqp==1.4.9 anyjson==0.3.3 asgiref==3.2.7 billiard==3.3.0.23 celery==3.1.26.post2 certifi==2019.9.11 cffi==1.13.2 chardet==3.0.4 cryptography==2.8 defusedxml==0.6.0 diff-match-patch==20181111 Django==2.1.11 django-import-export==1.2.0 django-redis==4.10.0 django-simpleui==3.4 django-timezone-field==3.1 djangorestframework==3.11.0 djangorestframework-jwt==1.11.0 dnspython==1.16.0 docopt==0.6.2 et-xmlfile==1.0.1 greenlet==0.4.15 idna==2.8 importlib-metadata==0.23 jdcal==1.4.1 kombu==3.0.37 MarkupPy==1.14 monotonic==1.5 more-itertools==7.2.0 odfpy==1.4.0 openpyxl==3.0.1 optionaldict==0.1.1 Pillow==6.2.1 pycparser==2.19 pycryptodome==3.9.4 PyJWT==1.7.1 PyMySQL==0.9.3 pyOpenSSL==19.1.0 python-crontab==2.4.0 python-dateutil==2.8.1 pytz==2019.3 PyYAML==5.1.2 records==0.5.3 redis==3.3.11 redlock-py==1.0.8 requests==2.22.0 six==1.13.0 SQLAlchemy==1.3.11 sqlparse==0.3.0 tablib==0.14.0 urllib3==1.25.7 vine==1.3.0 wechatpy==1.8.3 xlrd==1.2.0 xlwt==1.3.0 xmltodict==0.12.0 zipp==0.6.0
最終效果:
控制台輸出
微信開發工具控制台輸出