Django会话管理
会话管理&有状态服务:
1.什么是有状态服务
1.1 有状态服务
1.2 真实请求讲解
1.3 小程序中的状态
2.实现登陆功能
2.1 用户体系的建立
2.2 授权登陆功能的实现
3.如何实现有状态服务
3.1 小程序中的状态管理
3.2 复杂多变的用户状态管理
4.有状态服务的效果
4.1 有状态的首页实现
5.完整代码
1.什么是有状态服务
1.1 有状态服务
1.1.1 什么是状态?(有状态服务与无状态服务)
评判标准:上下文关系,有状态服务的凭证
1.1.2 HTTP协议中的状态
1.客户端发起一个空请求,服务端将请求的相关数据(上下文)保存并标记Session(凭证)。服务端将请求进行返回,客户端拿到Session后,记录到Cookie里面
2.客户端发起一个请求(带Cookie),服务端拿到Cookie后搜索该凭证的相关数据,并将其返回。
1.2 真实请求讲解:
1.2.1 首次请求: 没有携带任何Cookie,服务端返回set-Cookie,叫浏览器设置Cookie,下次请求将携带该Cooikie如下图所示。
1.2.2 尝试进行登陆请求: 最顶的红框是登陆页面,登陆完成后跳转首页;服务端返回set-cookie,叫浏览器设置凭证,接下来的跳转首页将携带该凭证(Cookie)。
1.2.3 跳转到首页: 可以查看到登陆后跳转到首页的请求携带了1.2.2的相关凭证(Cookie)
1.3 小程序中的状态
如上图所示,在小程序有状态服务器开发中,不能套用传统的cookie和session模型进行有状态服务的开发。
小程序和微信服务器不存在上下文关系
微信服务器和Django后台不存在上下文关系
小程序和Django后台不存在直接
上下文关系
如何解决呢?
由于微信服务器只对请求和响应进行转发,没有其他的数据处理和保存等,所以可以在小程序对cookie进行处理,同样服务端对session进行处理。具体看3.1节。
2.小程序的登陆功能
2.1 建立用户体系
2.1.1 自建用户体系VS沿用微信用户体系
自建用户体系:邮箱、手机号标记用户;需实现填写、去重等复杂的交互逻辑,需要进行注册、登陆等功能的开发;
沿用微信用户体系:openid
2.1.2 用户模型与用户视图
用户的唯一标识:openid
用户的基本信息:nickname
authorization/models.py
from django.db import models
# Create your models here.
class User(models.Model):
open_id = models.CharField(max_length=32, unique=True) # 微信的openid是长度为32的字符串
nickname = models.CharField(max_length=256) # 用户名
python manage.py makemigrations # 检测数据模型的变化,若有则生成文件来记录
python manage.py migrate #运行所有生成出来的文件来对数据库进行修改
authorization/views.py
from django.view import View
class UserView(View):
def get(self, request):
pass
def post(self, request):
pass
2.1.3 获取用户的OpenID
2.2 授权登陆功能的实现
小程序侧:
page/homepage.js
authorize: function(){
wx.login({
success: function(res){
var code =res.code
var appId = app.globalData.appId
var nickname = app.globalData.userInfo.nickName
wx.request({
url: app.globalData.serverUrl + app.globalData.apiVersion + '/auth/authorize',
method: 'POST',
data: {
code: code,
appId: appId,
nickname: nickname
},
header: {'content-type': 'application/json'},
success: function(res){
wx.showToast({
title: '授权成功',
})
var cookie = cookieUtil.getSessionIDFromResponse(res)
cookieUtil.setCookieToStorage(cookie)
console.log(cookie)
}
})
}
})
},
django后台:
authorization/views.py
def c2s(appid, code):
API = 'https://api.weixin.qq.com/sns/jscode2session'
params = 'appid=%s&secret=%s&js_code=%s&grant_type=authorization_code' % \
(appid, settings.WX_APP_SECRET, code)
url = API + '?' + params
response = requests.get(url=url)
data = json.loads(response.text)
print('data from weixin server:', data)
return data
def __authorize_by_code(request):
"""
使用wx.login到的临时code到微信提供的code2session接口授权
post_data = {
'encryptedData': 'xxx',
'appId': 'xxx',
'sessionKey': 'xxx',
'iv': 'xxx'
}
"""
post_data = request.body.decode('utf-8')
print('data from applet:', post_data)
post_data = json.loads(post_data)
code = post_data.get('code').strip()
app_id = post_data.get('appId').strip()
nickname = post_data.get('nickname').strip()
response = {}
# 需要微信小程序提供code和appid,然后将code、appid和secret提交给c2s向微信接口服务认证
if not code or not app_id:
response['message'] = 'lost code or appId'
response['code'] = ReturnCode.BROKEN_AUTHORIZED_DATA
return JsonResponse(data=response, safe=False)
data = c2s(app_id, code)
openid = data.get('openid')
print('get openid:', openid)
# 若没有从微信接口服务获取到openid,则认证失败
if not openid:
response = wrap_json_response(code=ReturnCode.UNAUTHORIZED, message='auth failed')
return JsonResponse(data=response, safe=False)
request.session['open_id'] = openid
request.session['is_authorized'] = True
# 认证成功后查看该用户是否在开发者数据存在,不存在则保存
if not User.objects.filter(open_id=openid):
new_user = User(open_id=openid, nickname=nickname)
new_user.save()
response = wrap_json_response(code=ReturnCode.SUCCESS, message='auth success.')
return JsonResponse(data=response, safe=False)
def authorize(request):
return __authorize_by_code(request)
3.如何实现有状态服务
3.1 小程序中的状态管理
3.1.1 小程序Storage存取Cookies
utils/cookies.js
const key = 'cookie'
//从response取Cookie
function getSessionIDFromResponse(res){
var cookie = res.header['Set-Cookie']
console.log('get cookie from response:' + cookie)
return cookie
}
//将Cookie存储到Storage中
function setCookieToStorage(cookie){
try{
wx.setStorageSync(key,cookie)
}catch(e){console.log(e)}
}
//从Storage中取Cookie
function getCookieFromStorage(){
var value = wx.getStorageSync(key)
return value
}
//导出即可在外部使用这三个函数
module.exports = {
setCookieToStorage: setCookieToStorage,
getCookieFromStorage: getCookieFromStorage,
getSessionIDFromResponse: getSessionIDFromResponse
}
page/homepage.js
// pages/homepage/homepage.js
const app = getApp()
const cookieUtil = require('../../utils/cookie.js')
Page({
/**
* 页面的初始数据
*/
data: {},
onReadCookies: function(){
wx.request({
url: '127.0.0.1:8000/api/v1/auth/test',
success: function(res){
var cookie = cookieUtil.getSessionIDFromResponse(res)
console.log(cookie)
cookieUtil.setCookieToStorage(cookie)
//二次带cookie访问
var newCookie = cookieUtil.getCookieFromStorage()
var header = {}
header.Cookie = newCookie
wx.request({
url: '127.0.0.1:8000/api/v1/auth/test2',
header: header,
success: function(res){}
})
}
})
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {}
})
3.1.2 Django的Session中间件
启用Session中间件
settings.py
MIDDLEWARE = [
......
'django.contrib.sessions.middleware.SessionMiddleware',
......
]
使用Session的方法
from django.shortcuts import render
# Create your views here.
from django.http import JsonResponse
# 首此请求设置客户端cookie
def test_session(request):
request.session['message'] = 'Test Django Session OK'
return JsonResponse(data='{"result_code": 0, "message": "success"}', safe=False)
# 二次请求打印客户端携带的cookie
def test_session2(request):
print(request.session.items())
return JsonResponse(data='{"result_code": 0, "message": "success"}', safe=False)
3.2 复杂多变的用户状态管理
Django后台侧
def logout(request):
request.session.clear()
response = wrap_json_response(code=ReturnCode.SUCCESS, message='logout')
return JsonResponse(data=response, safe=False)
设置session过期60秒:
settings.py
SESSION_COOKIE_AGE = 60*1
小程序侧
app.js
getAuthStatus: function(){
return this.globalData.auth.isAuthorized
},
setAuthStatus: function(status){
console.log('app.js/setAuthStatus: set auth status:', status)
if (status == true || status == false){
this.globalData.auth.isAuthorized = status
}else{
console.log('app.js/setAuthStatus: invalid status.')
}
},
homepage.js
//登录
authorize: function () {
console.log('authorize')
var that = this
// 登陆并获取cookie
wx.login({
success: function (res) {
console.log(res)
var code = res.code
var appId = app.globalData.appId
var nickname = app.globalData.userInfo.nickName
// 请求后台
wx.request({
url: app.globalData.serverUrl + app.globalData.apiVersion + '/auth/authorize',
method: 'POST',
data: {
code: code,
appId: appId,
nickname: nickname
},
header: {
'content-type': 'application/json' // 默认值
},
success: function(res) {
wx.showToast({
title: '授权成功',
})
// 保存cookie
var cookie = cookieUtil.getSessionIDFromResponse(res)
cookieUtil.setCookieToStorage(cookie)
that.setData({
isLogin: true,
userInfo: app.globalData.userInfo,
hasUserInfo: true
})
app.setAuthStatus(true)
}
})
}
})
},
//注销
logout: function(){
var that = this
var cookie = cookieUtil.getCookieFromStorage
var header = {}
header.Cookie = cookie
wx.request({
url: app.globalData.serverUrl+ app.globalData.apiVersion + '/auth/logout',
method: 'GET',
header: header,
success: function(res){
console.log('pages/homepage.js/logout: log', res)
that.setData({
isLogin: false,
userInfo: null,
hasUserInfo: false
})
cookieUtil.setCookieToStorage('')
app.setAuthStatus(false)
}
})
},
4.有状态服务的效果
4.1 有状态的首页实现
小程序侧:
//index.js
//获取应用实例
const app = getApp()
const cookieUtils = require('../../utils/cookie.js')
Page({
data: {
isAuthorized: false,
constellationData: null,
stockData: null,
weatherData: null,
userInfo: null,
hasUserInfo: null
},
//事件处理函数
bindViewTap: function() {
wx.navigateTo({
url: '../logs/logs'
})
},
updateData: function(){
wx.showLoading({title: '加载中',})
var that = this
var cookie = cookieUtils.getCookieFromStorage()
var header = {}
header.Cookie = cookie
wx.request({
url: app.globalData.serverUrl + app.globalData.apiVersion + '/service/weather',
method: 'GET',
header: header,
success: function(res){
that.setData({weatherData: res.data.data})
wx.hideLoading()
}
})
wx.request({
url: app.globalData.serverUrl + app.globalData.apiVersion + '/service/stock',
method: 'GET',
header: header,
success: function(res){
that.setData({stockData: res.data.data})
wx.hideLoading()
}
})
wx.request({
url: app.globalData.serverUrl + app.globalData.apiVersion + '/service/constellation',
method: 'GET',
header: header,
success: function (res) {
console.log(res)
that.setData({constellationData: res.data.data})
wx.hideLoading()
}
})
},
//下拉刷新,先检查session是否过期,再更新页面数据
onPullDownRefresh: function(){
var that = this
var cookie = cookieUtils.getCookieFromStorage()
var header = {}
header.Cookie = cookie
wx.request({
url: app.globalData.serverUrl+app.globalData.apiVersion+'/auth/status',
header: header,
success: function(res){
var data = res.data.data
if (data.is_authorized ==1){
that.setData({isAuthorized: true})
that.updateData()
}else{
that.setData({isAuthorized: false})
wx.showToast({
title: '请先登陆',
icon: 'none'
})
}
}
})
},
onLoad: function () {
this.onPullDownRefresh()
if (app.globalData.userInfo) {
this.setData({
userInfo: app.globalData.userInfo,
hasUserInfo: true
})
} else if (this.data.canIUse){
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
app.userInfoReadyCallback = res => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
} else {
// 在没有 open-type=getUserInfo 版本的兼容处理
wx.getUserInfo({
success: res => {
app.globalData.userInfo = res.userInfo
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
}
},
getUserInfo: function(e) {
console.log(e)
app.globalData.userInfo = e.detail.userInfo
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
},
teststorage: function(event){
wx.setStorage({
key: 'mykey',
data: 'mydata',
})
wx.getStorage({
key: 'mykey',
success: function(res) {
console.log(res.data)
},
})
}
})
Django后台侧
# 获得个人设置的天气信息
class WeatherView(View, CommonResponseMixin, WeatherParse):
def get(self, request):
if not already_authorized(request):
response = self.wrap_json_response({}, code=ReturnCode.UNAUTHORIZED)
else:
data = []
open_id = request.session.get('open_id')
user = User.objects.filter(open_id=open_id)[0]
cities = json.loads(user.focus_cities)
for city in cities:
city = re.sub(r'市', '', city['city'])
print(city)
result = self.get_weather_now(city)
result['city_info'] = city
data.append(result)
response = self.wrap_json_response(data=data, code=ReturnCode.SUCCESS)
return JsonResponse(data=response, safe=False)
popular_constellations = ['金牛座', '处女座', '天蝎座']
popular_stocks = [
{
'code': '000001',
'name': '平安银行',
'market': 'sz'
},
{
'code': '600036',
'name': '招商银行',
'market': 'sh'
},
{
'code': '601398',
'name': '工商银行',
'market': 'sh'
}
]
# 星座
def constellation(request):
if already_authorized(request):
user = get_user(request)
constellations = json.loads(user.focus_constellations)
else:
constellations = popular_constellations
print(constellations)
response = CommonResponseMixin.wrap_json_response(data=constellations, code=ReturnCode.SUCCESS)
return JsonResponse(data=response, safe=False)
# 股票
def stock(request):
if already_authorized(request):
user = get_user(request)
stock = json.loads(user.focus_stocks)
else:
stock = popular_stocks
print(stock)
response = CommonResponseMixin.wrap_json_response(data=stock, code=ReturnCode.SUCCESS)
return JsonResponse(data=response, safe=False)
def already_authorized(request):
if request.session.get('is_authorized'):
return True
else:
return False
def get_user(request):
if not already_authorized(request):
raise Exception('not authorized request')
open_id = request.session.get('open_id')
user = User.objects.filter(open_id=open_id)[0]
return user
5.完整代码
https://github.com/jstang007/wxapp