如今大多数软件公司的业务再也不是单条线,而是发展成多元化的产品线。包括多个网站应用、移动APP以及桌面软件,那么固然但愿能实现统一用户和统一登陆。统一用户基本都已实现,然而统一登陆却仍是有很多公司未予以实现。这倒不是说SSO有多复杂或多难实现,这其中可能有历史遗留问题也或是其它缘由。这是题外话,本文不做深究。javascript
统一用户指的是多个应用共用一套账号体系。好比Z公司旗下有aw和bw两个网站,有账号goal,那么使用账号goal能登陆aw和bw。这个在技术上也不难实现,一般来讲有2个方案:
html
共享持久层java
这是最经常使用的方式。aw和bw经过直接访问同一个后端数据库来达到数据共享。python
经过代理访问jquery
这种方式相似于经过网关来访问同一个后端数据库。本质上跟共享持久层是同样的,无外乎多了层网关,这样是有好处的,本文接下来会涉及到。web
这看起来好像够了,可是请您考虑这样一个场景:aw和bw是两个不一样的网页游戏,那么首先aw和bw都有本身的激活流程,而后有本身的游戏角色、装备等各类属性。因此aw和bw应该有各自的profile。对此我概括了几下几点:ajax
统一用户账号表仅保存各个应用的公共属性,其它应用能够重写这些属性算法
应该能标识出是否已激活过
shell
应该能标识出属于哪一个应用数据库
对于第一点,这没什么好说的。第二点和第三点能够分别用一个整型字段来表示,属性存储在不一样的位上,并且通常都是这么作的。以下代码所示:
#!/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以前的登陆流程是这样的:
aw和bw各自维护本身的登陆会话
aw的登陆不会致使bw登陆,相反也是如此
aw的退出不会致使bw的退出,相反也是如此
这种体验对用户来讲是极不友好的,明明是一样的账户,却不得不逐个去输入用户名和密码来登陆。SSO正好能够解决这些问题。SSO通常被用于web和web之间,但有时也被用于和桌面软件、移动APP之间的统一登陆。不过只有web和web之间才能算是标准的SSO,其它的却不是。接下来分别谈谈这几种方式的原理:
web和web之间单点登陆
web和桌面软件、移动APP之间单点登陆
对于使用session来保存登陆态想必各位都没有什么疑问,不明白的能够去自行 Google 。好比有站点aw和bw须要统一登陆,那么会出现2种状况:
aw和bw是二级子域名
例如aw和bw站点域名分别是aw.test.com和bw.test.com,那么其实能够设置session的cookie domain为.test.com来使aw和bw共享会话信息。这种方式不具有通用性而且简单,所以不做深究。
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种:
跳转方式
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方式是经过<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,并无办法去清除它的登陆态。
以前一直忽略了一个事实,所谓共享SSO登陆态,实际上是基于同一个UserAgent的。对于web应用来讲,UserAgent就是浏览器。这是由于浏览器之间没法共享cookie,而session是基于cookie的。
这个其实不能算严格意义上的SSO,只能算是代签。能够登陆2个QQ号来进行观察,登陆后在2个QQ上点击邮箱图标进入邮箱,您能够发现连接上被附加了一串sid。sid是session id的缩写,能够用来标识一个会话。您能够清楚的看到邮箱上的每一个连接都被附加上了一串sid参数,这是由于容许同时使用多个邮箱,若是设置登陆态的话则会覆盖前一个。这种方式看起来就像早期PHP不支持session的作法,每次经过传递sid到服务器端来解密进行标识用户。
对于移动APP的受权,有使用OAuth方式,也有使用传递sid方式,对此不做深究。
SSO能够同时支持传递sid、OAuth方式和登陆态方式的受权校验,并只能被受权后的应用使用SSO。