微信網(wǎng)頁授權(quán)基礎(chǔ)知識
網(wǎng)頁授權(quán)的最終目的就是獲取微信的用戶信息味悄,微信的網(wǎng)頁授權(quán)方式有兩種
- snsapi_base:只能獲取用戶的openid,靜默授權(quán)
- snsapi_userinfo:獲取用戶信息扩借,用戶手動同意授權(quán)(這種方式是這篇文章主要講述的)
微信網(wǎng)頁授權(quán)的序列圖
由于沒正式學(xué)過怎么花序列圖,該序列圖肯定是不合規(guī)范的,但整個交互過程還是寫清晰的了。需要明確的是在微信中用戶訪問網(wǎng)頁要經(jīng)過微信鸦难,后端返回?cái)?shù)據(jù)也是要經(jīng)過微信。
一些準(zhǔn)備工作
填寫好微信回調(diào)域名员淫,用戶訪問的頁面需要在該域名下合蔽。填錯了,用戶同意授權(quán)后介返,就會出現(xiàn)redirect_uri error辈末。填寫or修改地方在下圖位置
代碼書寫:
urls.py的編寫
#encoding=utf-8
from django.conf.urls import include, url
from .views import AuthView, GetUserInfoView, TestView, WxSignature
urlpatterns = [
# 授權(quán)
url(r'^auth/$', AuthView.as_view(), name='wx_auth'),
# 獲取用戶信息
url(r'^code/$', GetUserInfoView.as_view(), name='get_user_info'),
# 微信接口配置信息驗(yàn)證
url(r'^$', WxSignature.as_view(), name='signature'),
# 測試
url(r'^test/$', TestView.as_view(), name='test_view'),
]
判斷是否已經(jīng)有用戶信息
這里主要利用了View類的實(shí)現(xiàn)機(jī)制,一個url請求過來后映皆,會調(diào)用dispatch()方法挤聘,根據(jù)請求類型選取對應(yīng)的處理方法,例如, GET請求捅彻,就會調(diào)用View類下的get()方法∽槿ィ現(xiàn)在新建一個auth_view.py文件,里面創(chuàng)建一個名為AuthView繼承了View類的class步淹,并改寫dispatch()方法, 判斷的關(guān)鍵是看session中是否存在'user'這個key, 該信息在獲取到了用戶信息后添加到session當(dāng)中从隆,另外請求參數(shù)path保存用戶需要訪問的網(wǎng)頁的url地址。參數(shù)用來保存以后需要進(jìn)行要求的網(wǎng)頁缭裆,繼承AuthView即可進(jìn)行網(wǎng)頁授權(quán)
#encoding=utf-8
import urllib
from django.views.generic import View
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
import youhui.settings as setting
class AuthView(View):
def dispatch(self, request, *args, **kwargs):
# 判斷是否有授權(quán)
if not 'user' in request.session:
# 用戶需要訪問的url路徑
path = request.get_full_path()
# 跳轉(zhuǎn)url,
red_url = '%s?path=%s' % (reverse('wx_auth'), urllib.quote(path))
return redirect(red_url)
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
wechat_api.py
該微信api基本就只有網(wǎng)頁授權(quán)部分键闺,我把jeff大牛的wechat sdk, 中有用的部分抽離出來,并把網(wǎng)頁授權(quán)的加了上去澈驼。jeff大牛寫的wechat sdk在這里辛燥。授權(quán)部分在WechatApi類中, 創(chuàng)建時傳入appid和appsecret就可以使用。
主要方法說明:
_process_response: 解析微信返回的json數(shù)據(jù)缝其,返回相對應(yīng)的dict
auth_url: 返回網(wǎng)頁授權(quán)url
get_auth_access_token: 根據(jù)授權(quán)成功返回的code, 獲取網(wǎng)頁授權(quán)的access_token
get_user_info: 根據(jù)網(wǎng)頁授權(quán)的access_token獲取用戶信息
#encoding=utf-8
import requests
import simplejson
import urllib
import logging
log = logging.getLogger('django')
class APIError(object):
def __init__(self, code, msg):
self.code = code
self.msg = msg
def wx_log_error(APIError):
log.error('wechat api error: [%s], %s' % (APIError.code, APIError.msg))
class WechatBaseApi(object):
API_PREFIX = u'https://api.weixin.qq.com/cgi-bin/'
def __init__(self, appid, appsecret, api_entry=None):
self.appid = appid
self.appsecret = appsecret
self._access_token = None
self.api_entry = api_entry or self.API_PREFIX
@property
def access_token(self):
if not self._access_token:
token, err = self.get_access_token()
if not err:
self._access_token = token['access_token']
return self._access_token
else:
return None
return self._access_token
# 解析微信返回的json數(shù)據(jù)挎塌,返回相對應(yīng)的dict
def _process_response(self, rsp):
if 200 != rsp.status_code:
return None, APIError(rsp.status_code, 'http error')
try:
content = rsp.json()
except Exception:
return None, APIError(9999, 'invalid response')
if 'errcode' in content and content['errcode'] != 0:
return None, APIError(content['errcode'], content['errmsg'])
return content, None
def _get(self, path, params=None):
if not params:
params = {}
params['access_token'] = self.access_token
rsp = requests(self.api_entry + path, params=params)
return self._process_response(rsp)
def _post(self, path, data, type='json'):
header = {'content-type': 'application/json'}
if '?' in path:
url = self.api_entry + path + 'access_token=' + self.access_token
else:
url = self.api_entry + path + '?' + 'access_token=' + self.access_token
if 'json' == type:
data = simplejson.dumps(data, ensure_ascii=False).encode('utf-8')
rsp = requests.post(url, data, headers=header)
return self._process_response(rsp)
class WechatApi(WechatBaseApi):
def get_access_token(self, url=None, **kwargs):
params = {'grant_type': 'client_credential', 'appid': self.appid, 'secret': self.appsecret}
if kwargs:
params.update(kwargs)
rsp = requests.get(url or self.api_entry + 'token', params)
return self._process_response(rsp)
#返回授權(quán)url
def auth_url(self, redirect_uri, scope='snsapi_userinfo', state=None):
url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect' % \
(self.appid, urllib.quote(redirect_uri), scope, state if state else '')
return url
# 獲取網(wǎng)頁授權(quán)的access_token
def get_auth_access_token(self, code):
url = u'https://api.weixin.qq.com/sns/oauth2/access_token'
params = {
'appid': self.appid,
'secret': self.appsecret,
'code': code,
'grant_type': 'authorization_code'
}
return self._process_response(requests.get(url, params=params))
# 獲取用戶信息
def get_user_info(self, auth_access_token, openid):
url = u'https://api.weixin.qq.com/sns/userinfo?'
params = {
'access_token': auth_access_token,
'openid': openid,
'lang': 'zh_CN'
}
return self._process_response(requests.get(url, params=params))
view.py
這部分代碼實(shí)現(xiàn)了上面授權(quán)序列圖的步驟6到18,需要注意的是需要設(shè)置user信息到session中内边,當(dāng)然也可以設(shè)置其他信息榴都,在AuthView中判斷條件要與之相對應(yīng)即可。
#encoding=utf-8
import hashlib
import simplejson
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseServerError, Http404
from django.views.generic import View
from django.core.urlresolvers import reverse
from .models import User
from .serializers import UserSerializer
from .auth_view import AuthView as BaseView
from .wechat_api import WechatApi, wx_log_error
import youhui.settings as settings
from youhui.utils import log_err
# Create your views here.
class WecahtApiView(View):
# 填入公眾號appid, appsecret
APPID = settings.APPID
APPSECRET = settings.APPSECRET
HOST = settings.HOST
wechat_api = WechatApi(appid=APPID, appsecret=APPSECRET)
class WxSignature(View):
pass #非重點(diǎn)省略
class AuthView(WecahtApiView):
def get(self, request):
path = request.GET.get('path')
if path:
if 'user' in request.session:
return redirect(path)
else:
red_url = '%s%s?path=%s' % (self.HOST, reverse('wx:get_user_info'), path)
redirect_url = self.wechat_api.auth_url(red_url)
print 'auth_url', redirect_url
return redirect(redirect_url)
else:
return Http404('parameter path not founded!')
class GetUserInfoView(WecahtApiView):
def get(self, request):
redir_url = request.GET.get('path')
code = request.GET.get('code')
if redir_url and code:
# 獲取網(wǎng)頁授權(quán)access_token
token_data, error = self.wechat_api.get_auth_access_token(code)
if error:
wx_log_error(error)
return HttpResponseServerError('get access_token error')
# 獲取用戶信息信息
user_info, error = self.wechat_api.get_user_info(token_data['access_token'], token_data['openid'])
if error:
wx_log_error(error)
return HttpResponseServerError('get userinfo error')
# 存儲用戶信息
user = self._save_user(user_info)
if not user:
return HttpResponseServerError('save userinfo error')
# 用戶對象存入session
request.session['user'] = user
# 跳轉(zhuǎn)回目標(biāo)頁面
return redirect(redir_url)
# 用戶禁止授權(quán)后怎么操作
else:
return Http404('parameter path or code not founded!!')
def _save_user(self, data):
user = User.objects.filter(openid=data['openid'])
# 沒有則存儲用戶數(shù)據(jù)漠其,有返回用戶數(shù)據(jù)的字典
if 0 == user.count():
user_data = {
'nick': data['nickname'].encode('iso8859-1').decode('utf-8'),
'openid': data['openid'],
'avatar': data['headimgurl'],
'info': self._user2utf8(data),
}
if 'unionid' in data:
user_data.update('unionid', data.unionid)
try:
new_user = User(**user_data)
new_user.save()
user_data.update({'id': new_user.id})
return user_data
except Exception, e:
log_err(e)
return None
else:
# 把User對象序列化成字典嘴高,具體看rest_framework中得內(nèi)容
return UserSerializer(user[0]).data
# 解決中文顯示亂碼問題
def _user2utf8(self, user_dict):
utf8_user_info = {
"openid": user_dict['openid'],
"nickname": user_dict['nickname'].encode('iso8859-1').decode('utf-8'),
"sex": user_dict['sex'],
"province": user_dict['province'].encode('iso8859-1').decode('utf-8'),
"city": user_dict['city'].encode('iso8859-1').decode('utf-8'),
"country": user_dict['country'].encode('iso8859-1').decode('utf-8'),
"headimgurl": user_dict['headimgurl'],
"privilege": user_dict['privilege'],
}
if 'unionid' in user_dict:
utf8_user_info.update({'unionid': user_dict['unionid']})
return utf8_user_info
class TestView(BaseView):
def get(self, request):
return render(request, 'test.html')