单点登陆原理

1、单系统登陆机制

一、http无状态协议

  web应用采用browser/server架构,http做为通讯协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与以前或以后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系php

3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8

  但这也同时意味着,任何用户都能经过浏览器访问服务器资源,若是想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制java

二、会话机制

  浏览器第一次请求服务器,服务器建立一个会话,并将会话的id做为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是否是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联web

8a9fb230-d506-4b19-b821-4001c68c4588

  服务器在内存中保存会话对象,浏览器怎么保存会话id呢?你可能会想到两种方式redis

  1. 请求参数
  2. cookie

  将会话id做为每个请求的参数,服务器接收请求天然能解析参数得到会话id,并借此判断是否来自同一会话,很明显,这种方式不靠谱。那就浏览器本身来维护这个会话id吧,每次发送http请求时浏览器自动发送会话id,cookie机制正好用来作这件事。cookie是浏览器用来存储少许数据的一种机制,数据以”key/value“形式存储,浏览器发送http请求时自动附带cookie信息数据库

  tomcat会话机制固然也实现了cookie,访问tomcat服务器时,浏览器中能够看到一个名为“JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id,使用了cookie的请求响应过程以下图api

518293d9-64b2-459c-9d45-9f353c757d1f

三、登陆状态

  有了会话机制,登陆状态就好明白了,咱们假设浏览器第一次请求服务器须要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已受权”或者“已登陆”等等之类的状态,既然是会话的状态,天然要保存在会话对象中,tomcat在会话对象中设置登陆状态以下浏览器

1
2
HttpSession session = request.getSession();
session.setAttribute( "isLogin" , true );

  用户再次访问时,tomcat在会话对象中查看登陆状态tomcat

1
2
HttpSession session = request.getSession();
session.getAttribute( "isLogin" );

  实现了登陆状态的浏览器请求服务器模型以下图描述安全

70e396fa-1bf2-42f8-a504-ce20306e31fa

  每次请求受保护资源时都会检查会话对象中的登陆状态,只有 isLogin=true 的会话才能访问,登陆机制所以而实现。服务器

2、多系统的复杂性

  web系统早已从久远的单系统发展成为现在由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登陆、而后一个一个注销吗?就像下图描述的这样

6dfbb0b1-46c0-4945-a3bf-5f060fa80710

  web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。不管web系统内部多么复杂,对用户而言,都是一个统一的总体,也就是说,用户访问web系统的整个应用群与访问单个系统同样,登陆/注销只要一次就够了

9fe14ab3-4254-447b-b850-0436e628c254

  虽然单系统的登陆解决方案很完美,但对于多系统应用群已经再也不适用了,为何呢?

  单系统登陆解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(一般对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是全部cookie

4d58ccfa-0114-486d-bec2-c28f2f9eb513

  既然这样,为何不将web应用群中全部子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,而后将它们的cookie域设置为“baidu.com”,这种作法理论上是能够的,甚至早期不少多系统登陆就采用这种同域名共享cookie的方式。

  然而,可行并不表明好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,否则cookie的key值(tomcat为JSESSIONID)不一样,没法维持会话,共享cookie的方式是没法实现跨语言技术平台登陆的,好比java、php、.net系统之间;第三,cookie自己不安全。

  所以,咱们须要一种全新的登陆方式来实现多系统应用群的登陆,这就是单点登陆

3、单点登陆

  什么是单点登陆?单点登陆全称Single Sign On(如下简称SSO),是指在多系统应用群中登陆一个系统,即可在其余全部系统中获得受权而无需再次登陆,包括单点登陆与单点注销两部分

一、登陆

  相比于单系统登陆,sso须要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其余系统不提供登陆入口,只接受认证中心的间接受权。间接受权经过令牌实现,sso认证中心验证用户的用户名密码没问题,建立受权令牌,在接下来的跳转过程当中,受权令牌做为参数发送给各个子系统,子系统拿到令牌,即获得了受权,能够借此建立局部会话,局部会话登陆方式与单系统的登陆方式相同。这个过程,也就是单点登陆的原理,用下图说明

  下面对上图简要描述

  1. 用户访问系统1的受保护资源,系统1发现用户未登陆,跳转至sso认证中心,并将本身的地址做为参数
  2. sso认证中心发现用户未登陆,将用户引导至登陆页面
  3. 用户输入用户名密码提交登陆申请
  4. sso认证中心校验用户信息,建立用户与sso认证中心之间的会话,称为全局会话,同时建立受权令牌
  5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌建立与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登陆,跳转至sso认证中心,并将本身的地址做为参数
  11. sso认证中心发现用户已登陆,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌建立与用户的局部会话,返回受保护资源

  用户登陆成功以后,会与sso认证中心及各个子系统创建会话,用户与sso认证中心创建的会话称为全局会话,用户与各个子系统创建的会话称为局部会话,局部会话创建以后,用户访问子系统受保护资源将再也不经过sso认证中心,全局会话与局部会话有以下约束关系

  1. 局部会话存在,全局会话必定存在
  2. 全局会话存在,局部会话不必定存在
  3. 全局会话销毁,局部会话必须销毁

  你能够经过博客园、百度、csdn、淘宝等网站的登陆过程加深对单点登陆的理解,注意观察登陆过程当中的跳转url与参数

二、注销

  单点登陆天然也要单点注销,在一个子系统中注销,全部子系统的会话都将被销毁,用下面的图来讲明

3b139d2e-0b83-4a69-b4f2-316adb8997ce

  sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知全部注册系统执行注销操做

  下面对上图简要说明

  1. 用户向系统1发起注销请求
  2. 系统1根据用户与系统1创建的会话id拿到令牌,向sso认证中心发起注销请求
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出全部用此令牌注册的系统地址
  4. sso认证中心向全部注册系统发起注销请求
  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
  6. sso认证中心引导用户至登陆页面

4、部署图

  单点登陆涉及sso认证中心与众子系统,子系统与sso认证中心须要通讯以交换令牌、校验令牌及发起注销请求,于是子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登陆过程实质是sso客户端与服务端通讯的过程,用下图描述

fb29685c-487c-42b9-9ceb-6c7ee29e98c9

  sso认证中心与sso客户端通讯方式有多种,这里以简单好用的httpClient为例,web service、rpc、restful api均可以

5、实现

  只是简要介绍下基于java的实现过程,不提供完整源码,明白了原理,我相信大家能够本身实现。sso采用客户端/服务端架构,咱们先看sso-client与sso-server要实现的功能(下面:sso认证中心=sso-server)

  sso-client

  1. 拦截子系统未登陆用户请求,跳转至sso认证中心
  2. 接收并存储sso认证中心发送的令牌
  3. 与sso-server通讯,校验令牌的有效性
  4. 创建局部会话
  5. 拦截用户注销请求,向sso认证中心发送注销请求
  6. 接收sso认证中心发出的注销请求,销毁局部会话

  sso-server

  1. 验证用户的登陆信息
  2. 建立全局会话
  3. 建立受权令牌
  4. 与sso-client通讯发送令牌
  5. 校验sso-client令牌有效性
  6. 系统注册
  7. 接收sso-client注销请求,注销全部会话

  接下来,咱们按照原理来一步步实现sso吧!

一、sso-client拦截未登陆请求

  java拦截请求的方式有servlet、filter、listener三种方式,咱们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口,在doFilter()方法中加入对未登陆用户的拦截

1
2
3
4
5
6
7
8
9
10
11
12
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
     HttpServletRequest req = (HttpServletRequest) request;
     HttpServletResponse res = (HttpServletResponse) response;
     HttpSession session = req.getSession();
     
     if (session.getAttribute( "isLogin" )) {
         chain.doFilter(request, response);
         return ;
     }
     //跳转至sso认证中心
     res.sendRedirect( "sso-server-url-with-system-url" );
}

二、sso-server拦截未登陆请求

  拦截从sso-client跳转至sso认证中心的未登陆请求,跳转至登陆页面,这个过程与sso-client彻底同样

三、sso-server验证用户登陆信息

  用户在登陆页面输入用户名密码,请求登陆,sso认证中心校验用户信息,校验成功,将会话状态标记为“已登陆”

1
2
3
4
5
6
@RequestMapping ( "/login" )
public String login(String username, String password, HttpServletRequest req) {
     this .checkLoginInfo(username, password);
     req.getSession().setAttribute( "isLogin" , true );
     return "success" ;
}

四、sso-server建立受权令牌

  受权令牌是一串随机字符,以什么样的方式生成都没有关系,只要不重复、不易伪造便可,下面是一个例子

1
String token = UUID.randomUUID().toString();

五、sso-client取得令牌并校验

  sso认证中心登陆后,跳转回子系统并附上令牌,子系统(sso-client)取得令牌,而后去sso认证中心校验,在LoginFilter.java的doFilter()中添加几行

1
2
3
4
5
6
7
8
9
10
11
// 请求附带token参数
String token = req.getParameter( "token" );
if (token != null ) {
     // 去sso认证中心校验token
     boolean verifyResult = this .verify( "sso-server-verify-url" , token);
     if (!verifyResult) {
         res.sendRedirect( "sso-server-url" );
         return ;
     }
     chain.doFilter(request, response);
}

  verify()方法使用httpClient实现,这里仅简略介绍,httpClient详细使用方法请参考官方文档

1
2
HttpPost httpPost = new HttpPost( "sso-server-verify-url-with-token" );
HttpResponse httpResponse = httpClient.execute(httpPost);

六、sso-server接收并处理校验令牌请求

  用户在sso认证中心登陆成功后,sso-server建立受权令牌并存储该令牌,因此,sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过时,令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心(就是存储起来的意思)

  令牌与注册系统地址一般存储在key-value数据库(如redis)中,redis能够为key设置有效时间也就是令牌的有效期。redis运行在内存中,速度很是快,正好sso-server不须要持久化任何数据。

  令牌与注册系统地址能够用下图描述的结构存储在redis中,可能你会问,为何要存储这些系统的地址?若是不存储,注销的时候就麻烦了,用户向sso认证中心提交注销请求,sso认证中心注销全局会话,但不知道哪些系统用此全局会话创建了本身的局部会话,也不知道要向哪些子系统发送注销请求注销局部会话

3b221593-f9c4-45af-a567-4937786993e8

七、sso-client校验令牌成功建立局部会话

  令牌校验成功后,sso-client将当前局部会话标记为“已登陆”,修改LoginFilter.java,添加几行

1
2
3
if (verifyResult) {
     session.setAttribute( "isLogin" , true );
}

  sso-client还需将当前会话id与令牌绑定,表示这个会话的登陆状态与令牌相关,此关系能够用java的hashmap保存,保存的数据用来处理sso认证中心发来的注销请求

八、注销过程

  用户向子系统发送带有“logout”参数的请求(注销请求),sso-client拦截器拦截该请求,向sso认证中心发起注销请求

1
2
3
4
String logout = req.getParameter( "logout" );
if (logout != null ) {
     this .ssoServer.logout(token);
}

  sso认证中心也用一样的方式识别出sso-client的请求是注销请求(带有“logout”参数),sso认证中心注销全局会话

1
2
3
4
5
6
7
8
@RequestMapping ( "/logout" )
public String logout(HttpServletRequest req) {
     HttpSession session = req.getSession();
     if (session != null ) {
         session.invalidate(); //触发LogoutListener
     }
     return "redirect:/" ;
}

  sso认证中心有一个全局会话的监听器,一旦全局会话注销,将通知全部注册系统注销

1
2
3
4
5
6
7
8
public class LogoutListener implements HttpSessionListener {
     @Override
     public void sessionCreated(HttpSessionEvent event) {}
     @Override
     public void sessionDestroyed(HttpSessionEvent event) {
         //经过httpClient向全部注册系统发送注销请求
     }
}

(完)

做者:凌承一
出处:http://www.cnblogs.com/ywlaker/ 本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此声明,并在文章页面明显位置给出原文连接,不然做者将保留追究法律责任的权利。

相关文章
相关标签/搜索