Flask: SSO原理及實現

如今大多數軟件公司的業務再也不是單條線,而是發展成多元化的產品線。包括多個網站應用、移動APP以及桌面軟件,那麼固然但願能實現統一用戶和統一登陸。統一用戶基本都已實現,然而統一登陸卻仍是有很多公司未予以實現。這倒不是說SSO有多複雜或多難實現,這其中可能有歷史遺留問題也或是其它緣由。這是題外話,本文不做深究。javascript

什麼是統一用戶

統一用戶指的是多個應用共用一套賬號體系。好比Z公司旗下有aw和bw兩個網站,有賬號goal,那麼使用賬號goal能登陸aw和bw。這個在技術上也不難實現,一般來講有2個方案:
html

  1. 共享持久層java

    這是最經常使用的方式。aw和bw經過直接訪問同一個後端數據庫來達到數據共享。python

  2. 經過代理訪問jquery

    這種方式相似於經過網關來訪問同一個後端數據庫。本質上跟共享持久層是同樣的,無外乎多了層網關,這樣是有好處的,本文接下來會涉及到。web

這看起來好像夠了,可是請您考慮這樣一個場景:aw和bw是兩個不一樣的網頁遊戲,那麼首先aw和bw都有本身的激活流程,而後有本身的遊戲角色、裝備等各類屬性。因此aw和bw應該有各自的profile。對此我概括了幾下幾點:ajax

  1. 統一用戶賬號表僅保存各個應用的公共屬性,其它應用能夠重寫這些屬性算法

  2. 應該能標識出是否已激活過
    shell

  3. 應該能標識出屬於哪一個應用數據庫

對於第一點,這沒什麼好說的。第二點和第三點能夠分別用一個整型字段來表示,屬性存儲在不一樣的位上,並且通常都是這麼作的。以下代碼所示:

#!/usr/bin/env python
#coding: utf8

#已保存flag,從最低位算起,第一位表示aw,第二位表示bw
app_flag = 0x3
ac_flag  = 0x2

#測試某個應用是否已激活
if ac_flag & 0x1:
	print "aw已激活。"
elif ac_flag & 0x2:
	print "bw已激活。"

#進行激活aw應用
ac_flag |= 0x1

#測試是哪一個app
if app_flag & 0x1:
	print "這是aw應用。"
if app_flag & 0x2:
	print "這是bw應用。"

$ python test.py
bw已激活。
這是aw應用。
這是bw應用。

什麼是統一登陸

統一登陸又稱SSO(Single Sign On),即單點登陸。實現統一登陸的前提是已經實現了統一用戶。在實現SSO以前的登陸流程是這樣的:

  1. aw和bw各自維護本身的登陸會話

  2. aw的登陸不會致使bw登陸,相反也是如此

  3. aw的退出不會致使bw的退出,相反也是如此

這種體驗對用戶來講是極不友好的,明明是一樣的賬戶,卻不得不逐個去輸入用戶名和密碼來登陸。SSO正好能夠解決這些問題。SSO通常被用於web和web之間,但有時也被用於和桌面軟件、移動APP之間的統一登陸。不過只有web和web之間才能算是標準的SSO,其它的卻不是。接下來分別談談這幾種方式的原理:

  1. web和web之間單點登陸

  2. web和桌面軟件、移動APP之間單點登陸

web和web之間的單點登陸

原理

對於使用session來保存登陸態想必各位都沒有什麼疑問,不明白的能夠去自行 Google 。好比有站點aw和bw須要統一登陸,那麼會出現2種狀況:

  1. aw和bw是二級子域名

    例如aw和bw站點域名分別是aw.test.com和bw.test.com,那麼其實能夠設置session的cookie domain爲.test.com來使aw和bw共享會話信息。這種方式不具有通用性而且簡單,所以不做深究。

  2. aw和bw都是獨立的域名

    由於是2個獨立的域名,因此就不能經過設置session的cookie domain來實現了。SSO的作法就是將登陸態保存在SSO域(通常也稱passort或通行證)上,aw和bw的登陸、退出以及受權檢查都經過SSO來進行。本文將通篇使用aw, bw和SSO這三個站點來描述,而且使用Python的Flask框架來進行演示,若是沒有安裝Flask,請先安裝。

$ pip install flask

aw和bw是2個不一樣的web應用,都須要登陸才能訪問,而SSO就是爲aw和bw來提供服務的。爲此我配置了3個host,以下圖:

調用SSO的方式又能夠分爲如下2種:

  1. 跳轉方式

  2. ajax或jsonp方式

嚴格意義上來講,ajax和jsonp是屬於不一樣方式。由於ajax無法跨域去調用SSO,它須要經過服務器端代理的方式去調用SSO。而jsonp是能夠直接去調用SSO的,固然前提是SSO提供了jsonp方式的訪問。這樣劃分的依據只是爲了區分跳轉與非跳轉。

跳轉方式

當用戶在aw和bw未登陸時,則攜帶相應參數(如來源網址等)跳轉到SSO進行登陸,如登陸失敗則停留在SSO的登陸頁,登陸成功則SSO會生成ticket並附加給來源網址跳轉回去。固然SSO在跳轉回來源網址時會在SSO域上設置好登陸態。既然在SSO上設置登陸態,那麼在aw和bw上是否須要設置登陸態呢?答案是應該設置。舉例來講,若是aw跳轉到SSO進行登陸成功並在SSO上設置好登陸態後攜帶ticket跳轉回來,aw須要受權的頁面其實都是須要檢查用戶在aw上是否受權成功,若是不在aw上設置登陸態,則始終會跳轉到SSO去檢測受權,這樣的結果就是致使無限循環的跳轉,最終致使不可訪問。固然還有其它解決方案,那就是經過<script>或<iframe />來實現調用SSO檢測,但這是後話,將會在使用ajax或jsonp方式時進行講解。

如上所述,仍是應該在aw和bw上設置各自的登陸態,這樣在訪問aw時首先會在aw域上檢測受權,若是沒有受權,則跳轉到SSO進行登陸受權,登陸成功以後攜帶ticket跳轉回來。ticket是SSO爲這次登陸所生成的用戶基本信息加密串,來源域可經過解密ticket來獲取用戶基本信息,從而在來源域中設置登陸態。

可是aw和bw應該爲登陸態設置多長存活期呢?通常設爲瀏覽器進程存活期,也就是說aw和bw的登陸態的存活期直到瀏覽器關閉。SSO域上登陸態的存活期取決於具體的業務,本文中設爲30天。代碼以下:

aw代碼:

www/aw

----app.py

#coding: utf8
import os
from datetime import timedelta
from flask import Flask, session, redirect, url_for, request
import urllib

app = Flask(__name__)

app.secret_key = os.urandom(24)
app.permanent_session_lifetime = timedelta(seconds=24 * 60 * 60)

@app.route('/')
def index():
    #表示存活期爲瀏覽器進程的存活期
    session.permanent = False
    ticket = request.args.get('ticket', None)
    if ticket is not None:
        session['name'] = ticket.strip()
    #檢測登陸態
    if 'name' in session:
        return '登陸成功'
    else:
        referer = urllib.quote('http://www.aw.com:6666/')
        return redirect('http://www.sso.com:6668/login?referer=' + referer)

if __name__ == '__main__':
    app.run(
        host="0.0.0.0",
        port=int("6666"),
        debug=True
    )

bw代碼:

www/bw

----app.py

#coding: utf8
import os
from datetime import timedelta
from flask import Flask, session, redirect, url_for, request
import urllib

app = Flask(__name__)

app.secret_key = os.urandom(24)
app.permanent_session_lifetime = timedelta(seconds=24 * 60 * 60)

@app.route('/')
def index():
    #表示存活期爲瀏覽器進程的存活期
    session.permanent = False
    ticket = request.args.get('ticket', None)
    if ticket is not None:
        session['name'] = ticket.strip()
    #檢測登陸態
    if 'name' in session:
        return '登陸成功'
    else:
        referer = urllib.quote('http://www.bw.com:6667/')
        return redirect('http://www.sso.com:6668/login?referer=' + referer)

if __name__ == '__main__':
    app.run(
        host="0.0.0.0",
        port=int("6667"),
        debug=True
    )

sso代碼:

www/sso

----app.py

----templates

--------login.html

app.py源碼:

#coding: utf8
import os
from datetime import timedelta
from flask import Flask, session, render_template, request, redirect
import urllib

app = Flask(__name__)

app.secret_key = os.urandom(24)
app.permanent_session_lifetime = timedelta(seconds=30 * 24 * 60 * 60)

@app.route('/login')
def login():
    session.permanent = True
    referer = request.args.get('referer', None)
    if referer is not None:
        referer = referer.strip()
    if 'name' in session:
        if referer is not None:
            return redirect(referer + '?ticket=' + _makeTicket())
    return render_template('login.html', **dict(referer=referer))

@app.route('/dologin')
def doLogin():
    '''這裏其實忽略了判斷是否登陸的流程'''
    session.permanent = True
    referer = request.args.get('referer', None)
    if referer is not None:
        referer = urllib.unquote(referer.strip())
    #不實現登陸功能,直接設置登陸態
    _setLoginState()
    if referer:
        return redirect(referer + '?ticket=' + _makeTicket())
    else:
        return 'error'

def _setLoginState():
    session['name'] = 'goal'

def _makeTicket():
    '''生成ticket,這裏只是簡單返回用戶名,真實場景中可使用des之類的加密算法'''
    return 'goal'

if __name__ == '__main__':
    app.run(
        host="0.0.0.0",
        port=int("6668"),
        debug=True
    )

login.html源碼:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>SSO</title>
    <meta name="author" content="" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <meta name="keywords" content="SSO" />
    <meta name="description" content="SSO" />
</head>
<body>
<a href="{{ url_for('doLogin') }}{% if referer %}?referer={{ referer }}{% endif %}">請登陸</a>
</body>
</html>

$ python aw/app.py
 * Running on http://0.0.0.0:6666/
 * Restarting with reloader
$ python bw/app.py
 * Running on http://0.0.0.0:6667/
 * Restarting with reloader
$ python sso/app.py
 * Running on http://0.0.0.0:6668/
 * Restarting with reloader

打開aw站點,發現未登陸,則跳轉到SSO,點擊登陸成功後SSO設置登陸態並跳轉回aw並攜帶上ticket,aw根據ticket設置登陸態。流程對於bw也一樣適用。若是關閉瀏覽器,則aw和bw所設置的登陸態失效,但SSO上設置的並未過時,所以重啓瀏覽器打開aw站點將導至跳轉到SSO,而且在SSO上受權檢測成功,以後再一樣設置aw的登陸態。

以上是基於跳轉的方式實現的SSO,對於退出登陸也能夠經過一樣的方式來實現。

ajax或jsonp方式

對於ajax和jsonp方式來講,這只是請求登陸接口的不一樣方案。由於ajax不能跨域請求,因此須要服務器端代爲請求並將結果返回,而jsonp方式是經過<script>標記調用遠程腳原本實現的,若是SSO支持jsonp方式,則應優先選用。登陸請求過程比較簡單,ajax就沒什麼好說的,由於太經常使用了。對於jsonp來講,遠程執行完畢會返回一段JS代碼,一般是返回一個變量的定義,那麼咱們就能夠利用這個變量來拿到ticket併爲應用設置登陸態。

可是試想下,這種非跳轉方式須要跨域設置SSO的登陸態,那麼這實際上是能夠經過<script>和<iframe>來實現的。對於IE來講,還須要設置p3p頭部,而其它瀏覽器則不須要設置。在這點上實際上是IE遵循了隱私規範,咱們不妨爲IE點個贊。本文不打算實現ajax和jsonp方式的登陸,若是各位有問題,能夠一塊兒討論。

本文將經過<script>的方式對SSO進行跨域設置登陸態。很顯然,SSO須要提供一個URL調用給應用,而且SSO能夠提供一個JS腳本供應用使用,這樣就不須各個應用再去實現一遍了。OK,讓咱們先清除SSO上的會話信息,再重啓瀏覽器。代碼以下:

www/aw

----app.py

----templates

--------index.html

app.py源碼:

#coding: utf8
import os
from datetime import timedelta
from flask import Flask, session, request, render_template
import urllib

app = Flask(__name__)

app.secret_key = os.urandom(24)
app.permanent_session_lifetime = timedelta(seconds=24 * 60 * 60)

@app.route('/')
def index():
    session.permanent = False
    return render_template('index.html')

if __name__ == '__main__':
    app.run(
        host="0.0.0.0",
        port=int("6666"),
        debug=True
    )

index.html源碼:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>aw</title>
    <meta name="author" content="" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <meta name="keywords" content="aw" />
    <meta name="description" content="aw" />
    <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="http://www.sso.com:6668/static/sso.js"></script>
</head>
<body>
</body>
</html>

www/sso

----app.py

----static

--------sso.js

app.py源碼:

#coding: utf8
import os
from datetime import timedelta
from flask import Flask, session, request, make_response
import urllib

app = Flask(__name__)

app.secret_key = os.urandom(24)
app.permanent_session_lifetime = timedelta(seconds=30 * 24 * 60 * 60)

@app.route('/setLoginState')
def setLoginState():
    session.permanent = True
    session['name'] = 'goal'
    session['nick'] = '陳一回'
    resp = make_response('')
    resp.headers['P3P'] = 'CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"'
    return resp

@app.route('/test')
def test():
    session.permanent = True
    _str = ''
    if 'name' in session:
        _str = session['name']
    if 'nick' in session:
        _str += '---' + session['nick']
    return _str

if __name__ == '__main__':
    app.run(
        host="0.0.0.0",
        port=int("6668"),
        debug=True
    )

sso.js源碼:

$(function() {
	$.getScript("http://www.sso.com:6668/setLoginState", function() {
		console.log('success.');
	});
});

$ python aw/app.py
 * Running on http://0.0.0.0:6666/
 * Restarting with reloader
$ python sso/app.py
 * Running on http://0.0.0.0:6668/
 * Restarting with reloader

經過訪問 http://www.aw.com:6666  來設置SSO的登陸態,以後能夠經過 http://www.sso.com:6668/test  來查看輸出,結果很明顯是設置成功的。關於非跳轉方式的受權檢測以及退出登陸也是大同小異的,明白了原理,實現起來就很簡單了。

統一退出

統一退出的概念便是任何一方應用退出登陸,在清除應用自身的登陸態時也清除SSO的登陸態。這看起來沒有什麼問題,舉個例來講,aw和bw都登陸過,也就說aw、bw和SSO都設置過登陸態。aw的退出將會清除aw和SSO的登陸態,但bw還在會話期內,除非瀏覽器被關閉,不然bw仍是會處於登陸狀態的。解決方案也是有的,在aw退出時,順便也清除bw的登陸態(可經過遠程URL調用和P3P結合的方式來實現)。但若是SSO關聯的應用很是多,那麼退出的過程也變得漫長。有些公司的網站甚至是經過跳轉方式來進行逐一清除登陸態,這個沒有完美的解決方案,關鍵在於取捨。

統一受權檢測

以前所述的aw和bw自己也會設置登陸態。若是不想設置登陸態,則能夠經過SSO實現JS API供aw和bw來調用,在每一個頁面中經過遠程URL調用SSO的受權檢測。但這樣很明顯是弊大於利,不只會令SSO的請求數呈指數級增加,而且增長了aw和bw的編碼難度。

強制退出

考慮這麼一種場景。基於同一個用戶,在A電腦上登陸了aw,以後沒有關閉瀏覽器,而後在B電腦上也登陸了aw。那麼可否強制A電腦上的用戶退出呢?這個退出分爲SSO的退出和aw的退出。令SSO的退出是能夠實現的,只要在登陸態中保存登陸時間戳,服務器端持有用戶標識到登陸態的映射,那麼B電腦上的登陸會令登陸態和服務器端的映射同步,而A電腦上的登陸態將會過時。這個時候若是在A電腦上開啓bw(以前未登陸),則會跳轉到SSO,很明顯,這個時候A電腦上的SSO將是未受權狀態。對於A電腦上的aw,並無辦法去清除它的登陸態。

UserAgent

以前一直忽略了一個事實,所謂共享SSO登陸態,實際上是基於同一個UserAgent的。對於web應用來講,UserAgent就是瀏覽器。這是由於瀏覽器之間沒法共享cookie,而session是基於cookie的。

web和桌面軟件、移動APP之間單點登陸

這個其實不能算嚴格意義上的SSO,只能算是代簽。能夠登陸2個QQ號來進行觀察,登陸後在2個QQ上點擊郵箱圖標進入郵箱,您能夠發現連接上被附加了一串sid。sid是session id的縮寫,能夠用來標識一個會話。您能夠清楚的看到郵箱上的每一個連接都被附加上了一串sid參數,這是由於容許同時使用多個郵箱,若是設置登陸態的話則會覆蓋前一個。這種方式看起來就像早期PHP不支持session的作法,每次經過傳遞sid到服務器端來解密進行標識用戶。

對於移動APP的受權,有使用OAuth方式,也有使用傳遞sid方式,對此不做深究。

SSO能夠同時支持傳遞sid、OAuth方式和登陸態方式的受權校驗,並只能被受權後的應用使用SSO。

相關文章
相關標籤/搜索