首页 > 代码库 > php sso 转转
php sso 转转
PHP使用P3P完成COOKIE跨域操作
实际实用中,类似的需求有,比如说我们有两个域名,我们想实现在一个域名登录后,能自动完成另一个域名的登录,也就是单点登录(SSO)功能。
为了测试的方便,先编辑hosts文件,加入测试域名
sudo vim /etc/hosts
192.168.1.112 www.a.com
192.168.1.112 www.b.com
代码实现
www.a.com域名下的代码文件:
[a_setcookie.php]
<?php
//header(‘P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"‘);
setcookie("test", $_GET[‘id‘], time()+3600, "/", ".a.com");
?>
[a_getcookie.php]
<?php
var_dump($_COOKIE);
?>
www.b.com域名下的代码文件:
[b_setcookie.php]
<?php
//header(‘P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"‘);
setcookie("test", $_GET[‘id‘], time()+3600, "/", ".b.com");
?>
[b_getcookie.php]
<?php
var_dump($_COOKIE);
?>
依次访问
http://www.b.com/b_setcookie.php
http://www.a.com/a_getcookie.php
会发现a.com域上已经有cookie值了
代码分析
在www.b.com的域名下给www.a.com创建cookie。
若用户登录到www.b.com中,由此域名的b_setcookie.html中js实现方式给www.a.com域名设置cookie。
假设www.a.com域名下的a_getcookie.php有cookie则设定www.a.com登录成功。
b_setcookie.html:
<script src="http://www.a.com/a_setcookie.php?id=www.b.com"></script>
总结P3P的在上述代码中最主要的职责是:
跨域产生 cookie
注:上述代码在非IE下测试,即使不发送P3P头信息,也能成功。IE浏览器必需发送P3P才能成功!所以要跨域产生cookie还是有必要发送P3P的,毕竟IE的用户群体还是很大的。
参考:
http://my.oschina.net/goal/blog/199978
####--------------------------
摘要: 现在大多数软件公司的业务不再是单条线,而是发展成多元化的产品线。包括多个网站应用、移动APP以及桌面软件,那么当然希望能实现统一用户和统一登录。统一用户基本都已实现,然而统一登录却还是有不少公司未予以实现。这倒不是说SSO有多复杂或多难实现,这其中可能有历史遗留问题也或是其它原因。这是题外话,本文不作深究。
现在大多数软件公司的业务不再是单条线,而是发展成多元化的产品线。包括多个网站应用、移动APP以及桌面软件,那么当然希望能实现统一用户和统一登录。统一用户基本都已实现,然而统一登录却还是有不少公司未予以实现。这倒不是说SSO有多复杂或多难实现,这其中可能有历史遗留问题也或是其它原因。这是题外话,本文不作深究。
什么是统一用户
统一用户指的是多个应用共用一套帐号体系。比如Z公司旗下有aw和bw两个网站,有帐号goal,那么使用帐号goal能登录aw和bw。这个在技术上也不难实现,通常来说有2个方案:
共享持久层
这是最常用的方式。aw和bw通过直接访问同一个后端数据库来达到数据共享。
通过代理访问
这种方式类似于通过网关来访问同一个后端数据库。本质上跟共享持久层是一样的,无外乎多了层网关,这样是有好处的,本文接下来会涉及到。
这看起来好像够了,但是请您考虑这样一个场景:aw和bw是两个不同的网页游戏,那么首先aw和bw都有自己的激活流程,然后有自己的游戏角色、装备等各种属性。所以aw和bw应该有各自的profile。对此我归纳了几下几点:
统一用户帐号表仅保存各个应用的公共属性,其它应用可以重写这些属性
应该能标识出是否已激活过
应该能标识出属于哪个应用
对于第一点,这没什么好说的。第二点和第三点可以分别用一个整型字段来表示,属性存储在不同的位上,而且一般都是这么做的。如下代码所示:
#!/usr/bin/env python#coding: utf8#已保存flag,从最低位算起,第一位表示aw,第二位表示bwapp_flag = 0x3ac_flag = 0x2#测试某个应用是否已激活if ac_flag & 0x1: print "aw已激活。"elif ac_flag & 0x2: print "bw已激活。"#进行激活aw应用ac_flag |= 0x1#测试是哪个appif app_flag & 0x1: print "这是aw应用。"if app_flag & 0x2: print "这是bw应用。"
$ python test.pybw已激活。这是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之间单点登录
web和web之间的单点登录
原理
对于使用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: utf8import osfrom datetime import timedeltafrom flask import Flask, session, redirect, url_for, requestimport urllibapp = 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: utf8import osfrom datetime import timedeltafrom flask import Flask, session, redirect, url_for, requestimport urllibapp = 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: utf8import osfrom datetime import timedeltafrom flask import Flask, session, render_template, request, redirectimport urllibapp = 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‘) }