背景
SaaS 作爲一種服務,需要爲不同的客戶定製不同的域名以滿足客戶定製化的需求。而微信登錄時需要填寫一個回調地址,單一的回調地址是難以處理多客戶域名的業務需求的,經過不同的 SaaS 項目的實踐,總結出了下面的方式。
微信登錄的核心代碼依然採用 psa 這個庫 https://github.com/python-soc...。
微信說明
閱讀微信公衆平臺文檔,可以看到,當同一個微信公衆號需要在多個服務間使用時,微信的建議是提供一臺中控服務器,防止access_token的重複刷新,這個坑確實踩到過。
oauth 2.0
https://tools.ietf.org/html/r...
核心概念、表結構
中控機
中控機爲同一引導用戶登錄的微信登錄服務器,其中此機器做的爲 oauth 2.0 截圖部分的 A、B,引導用戶授權,微信回調到此中控機,拿到code。
中控機通過state參數,解出customerid,根據customer配置找到回調地址。回調是將state,code帶去回調的客戶域名。
customer
customer表需要記錄微信的appid,appsecret,這樣即使客戶需要定製自己的微信公衆號,中控機也可以saas化。
redirecturl
由於微信的state參數長度有限,因此提供一張redirecturl表記錄回調地址,登錄時只需要將redirecturl_id帶入state中即可。redirecturl記錄的爲回調客戶域名+psa complete地址的完整路由。
state
state爲oauth 2.0中允許的回調參數,state的構成爲: 客戶id,回調地址id,其他需要回調參數
核心流程
核心代碼
中控機通過customer獲取對應的appid,secret。微信回調到cherrypick後,拿着code,state跳轉到對應的客戶域名。
def _auth(request, backend):
cid = request.GET['cid']
# TODO: DoesNotExist
customer = Customer.objects.get(id=cid)
appid, appsecret = customer.get_key_and_secret()
log.info('login cid:%s, key:%s', cid, appid)
def get_key_and_secret():
return appid, appsecret
request.backend.get_key_and_secret = get_key_and_secret
return do_auth(request.backend)
@never_cache
@psa('our_social:cherrypick')
def auth(request, backend, key=''):
return _auth(request, backend)
@never_cache
@psa()
def cherrypick(request, backend):
code = request.GET.get('code', '')
state = request.GET.get('state', '')
redirect_url_id = state.split(',')[0]
redirect_url = RedirectURL.objects.get(id=redirect_url_id).url
redirect_url = '{}?code={}&state={}'.format(redirect_url, code, state)
log.info('cherrypick, redirect_url: %s, state: %s', redirect_url, state)
return redirect(redirect_url)
SaaS 服務器處理 oauth 2.0 C、D之後的步驟
@psa('our_social:complete')
def complete(request, backend, *args, **kwargs):
"""Authentication complete view"""
logout(request)
state = request.GET.get('state', '')
......
state解析出cid等參數
customer = Customer.objects.get(id=cid)
appid, appsecret = product.get_key_and_secret()
request._customer = customer
覆蓋backend的方法
def get_key_and_secret():
log.info('login complete use appid: %s %s', appid, state)
request.backend.get_key_and_secret = get_key_and_secret
return do_complete(request.backend, _do_login, request.user,
redirect_name=REDIRECT_FIELD_NAME, request=request,
*args, **kwargs)