zoukankan      html  css  js  c++  java
  • php sso 转转

    http://www.cnblogs.com/super-d2/p/4719660.html

    PHP不同域名cookie共享(单点登录实现原理)

     

    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个方案:

    1. 共享持久层

      这是最常用的方式。aw和bw通过直接访问同一个后端数据库来达到数据共享。

    2. 通过代理访问

      这种方式类似于通过网关来访问同一个后端数据库。本质上跟共享持久层是一样的,无外乎多了层网关,这样是有好处的,本文接下来会涉及到。

    这看起来好像够了,但是请您考虑这样一个场景:aw和bw是两个不同的网页游戏,那么首先aw和bw都有自己的激活流程,然后有自己的游戏角色、装备等各种属性。所以aw和bw应该有各自的profile。对此我归纳了几下几点:

    1. 统一用户帐号表仅保存各个应用的公共属性,其它应用可以重写这些属性

    2. 应该能标识出是否已激活过

    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。

  • 相关阅读:
    开放源码的对象关系映射工具ORM.NET 插入数据 Insert/Update Data
    开放源码的对象关系映射工具ORM.NET 快档开发入门 Quick Start
    .NET 动态脚本语言Script.NET 开发指南
    开放源码的对象关系映射工具ORM.NET 删除数据 Deleting Records using ORM.NET
    .NET Remoting过时了吗?为什么公司的项目还是选择用.NET Remoting,而不是WCF?
    开放源码的对象关系映射工具ORM.NET 查看和显示数据 View and Display data using ORM.NET
    开放源码的对象关系映射工具ORM.NET 查询表 调用存储过程 增加自定义代码
    技术人生:坚持,每日一博
    CQRS:CQRS + DDD + MDP 实现快速应用程序开发
    NodeJs:Happy代码生成器,重构了代码,更新了文档,完善了示例,欢迎下载使用
  • 原文地址:https://www.cnblogs.com/kaka100/p/6204030.html
Copyright © 2011-2022 走看看