本文由 简悦 SimpRead 转码, 原文地址 www.zhihu.com 云山雾隐
不兜圈子,方式有以下几种:
1.CAS 中央认证服务
CAS(Central Authentication Service) 中央认证服务,是一种独立开放指令协议,旨在为 Web 应用系统提供单点登录 / 单点登出的方法,简单但功能强大。同时也是基于票证 (ticket) 的协议,涉及一个或多个客户端和一台服务器;CAS Client 支持众多客户端,包括 Java、Net、PHP、Perl、Apache 等语言编写的各种 web 应用。用户向中央 CAS Server 应用程序提供一次凭据 (用户 ID 和密码),就可以访问多个应用程序,适用于开源的企业级单点登录。
2.OAuth 2 开放授权
OAuth 2(开放授权),是一个开放标准。允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
3.SAML 2.0
SAML(Security Assertion Markup Language),即安全断言标记语言 ,它是一个基于 XML 的标准,可用于在不同的安全域之间交换认证和授权数据。在 SAML 标准定义了身份提供者 (identity provider) 和服务提供者 (service provider),这两者构成了前面所说的不同的安全域。
4.OIDC
OIDC(OpenID Connect),即在 OAuth2 的基础上融入身份认证,是一个基于 OAuth2 协议的身份认证标准协议,完全兼容 OAuth2。OIDC 的核心在于其授权流程中,一并提供用户的身份认证信息给到第三方客户端,身份认证信息使用 JWT 格式来包装,得益于 JWT 的自包含性、紧凑性以及防篡改机制,使得身份认证信息可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了 UserInfo 的接口,用于获取用户的更完整的信息。
5.JWT
JWT(Json Web Token),是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准,该 Token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证和加密。
结论
若需要为自研产品开发单点登录,结合安全性的情况下,建议使用兼容场景全面、应用最广泛的 SAML 2.0 或 OAuth 2。但不论采用哪一种单点登录方案,带来便利的同时必然会带来一些问题,如账号丢失带来的安全风险、单点登录模块开发维护成本等。
端隐零信任安全解决方案采用了零信任的 ” 持续验证,永不信任 “ 的网络安全防护理念,其中可信身份认证系统模块支持账号全生命周期管理,并广泛支持单点登录方式,如 SAML 2.0、OIDC、SDK 等,可以联动企业已有的安全设备及软件,为企业构筑一条” 用户可信、设备可信、链路可信及资源可信 “构成的信任链。
老刘
一、单系统登录机制
1、http 无状态协议
web 应用采用 browser/server 架构,http 作为通信协议。http 是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求 / 响应对之间没有任何联系
但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然 http 协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。
2、会话机制
浏览器第一次请求服务器,服务器创建一个会话,并将会话的 id 作为响应的一部分发送给浏览器,浏览器存储会话 id,并在后续第二次和第三次请求中带上会话 id,服务器取得请求中的会话 id 就知道是不是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联。
服务器在内存中保存会话对象,浏览器怎么保存会话 id 呢?你可能会想到两种方式
- 请求参数
- cookie
将会话 id 作为每一个请求的参数,服务器接收请求自然能解析参数获得会话 id,并借此判断是否来自同一会话,很明显,这种方式不靠谱。那就浏览器自己来维护这个会话 id 吧,每次发送 http 请求时浏览器自动发送会话 id,cookie 机制正好用来做这件事。cookie 是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送 http 请求时自动附带 cookie 信息
tomcat 会话机制当然也实现了 cookie,访问 tomcat 服务器时,浏览器中可以看到一个名为 “JSESSIONID” 的 cookie,这就是 tomcat 会话机制维护的会话 id,使用了 cookie 的请求响应过程如下图:
3、登录状态
有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为 “已授权” 或者 “已登录” 等等之类的状态,既然是会话的状态,自然要保存在会话对象中,tomcat 在会话对象中设置登录状态如下
|
|
用户再次访问时,tomcat 在会话对象中查看登录状态
|
|
实现了登录状态的浏览器请求服务器模型如下图描述
每次请求受保护资源时都会检查会话对象中的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此而实现。
二、多系统的复杂性
web 系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样
![](data:image/svg+xml;utf8,)
web 系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论 web 系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问 web 系统的整个应用群与访问单个系统一样,登录 / 注销只要一次就够了。
![](data:image/svg+xml;utf8,)
虽然单系统的登录解决方案很完美,但对于多系统应用群已经不再适用了,为什么呢?
单系统登录解决方案的核心是 cookie,cookie 携带会话 id 在浏览器与服务器之间维护会话状态。但 cookie 是有限制的,这个限制就是 cookie 的域(通常对应网站的域名),浏览器发送 http 请求时会自动携带与该域匹配的 cookie,而不是所有 cookie
既然这样,为什么不将 web 应用群中所有子系统的域名统一在一个顶级域名下,例如 “*.baidu.com”,然后将它们的 cookie 域设置为 “baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享 cookie 的方式。
然而,可行并不代表好,共享 cookie 的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是 web 服务器)要相同,不然 cookie 的 key 值(tomcat 为 JSESSIONID)不同,无法维持会话,共享 cookie 的方式是无法实现跨语言技术平台登录的,比如 java、php、.net 系统之间;第三,cookie 本身不安全。
因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录
三、单点登录
什么是单点登录?单点登录全称 Single Sign On(以下简称 SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。
1、登录
相比于单系统登录,sso 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso 认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明
下面对上图简要描述
- 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
- sso 认证中心发现用户未登录,将用户引导至登录页面
- 用户输入用户名密码提交登录申请
- sso 认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话,同时创建授权令牌
- sso 认证中心带着令牌跳转会最初的请求地址(系统 1)
- 系统 1 拿到令牌,去 sso 认证中心校验令牌是否有效
- sso 认证中心校验令牌,返回有效,注册系统 1
- 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
- 用户访问系统 2 的受保护资源
- 系统 2 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
- sso 认证中心发现用户已登录,跳转回系统 2 的地址,并附上令牌
- 系统 2 拿到令牌,去 sso 认证中心校验令牌是否有效
- sso 认证中心校验令牌,返回有效,注册系统 2
- 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与 sso 认证中心及各个子系统建立会话,用户与 sso 认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过 sso 认证中心,全局会话与局部会话有如下约束关系
- 局部会话存在,全局会话一定存在
- 全局会话存在,局部会话不一定存在
- 全局会话销毁,局部会话必须销毁
你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解,注意观察登录过程中的跳转 url 与参数
2、注销
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明
sso 认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作
下面对上图简要说明
- 用户向系统 1 发起注销请求
- 系统 1 根据用户与系统 1 建立的会话 id 拿到令牌,向 sso 认证中心发起注销请求
- sso 认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
- sso 认证中心向所有注册系统发起注销请求
- 各注册系统接收 sso 认证中心的注销请求,销毁局部会话
- sso 认证中心引导用户至登录页面
四、部署图
单点登录涉及 sso 认证中心与众子系统,子系统与 sso 认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成 sso 的客户端,sso 认证中心则是 sso 服务端,整个单点登录过程实质是 sso 客户端与服务端通信的过程,用下图描述
sso 认证中心与 sso 客户端通信方式有多种,这里以简单好用的 httpClient 为例,web service、rpc、restful api 都可以
五、实现
只是简要介绍下基于 java 的实现过程,不提供完整源码,明白了原理,我相信你们可以自己实现。sso 采用客户端 / 服务端架构,我们先看 sso-client 与 sso-server 要实现的功能(下面:sso 认证中心 = sso-server)
sso-client
- 拦截子系统未登录用户请求,跳转至 sso 认证中心
- 接收并存储 sso 认证中心发送的令牌
- 与 sso-server 通信,校验令牌的有效性
- 建立局部会话
- 拦截用户注销请求,向 sso 认证中心发送注销请求
- 接收 sso 认证中心发出的注销请求,销毁局部会话
sso-server
- 验证用户的登录信息
- 创建全局会话
- 创建授权令牌
- 与 sso-client 通信发送令牌
- 校验 sso-client 令牌有效性
- 系统注册
- 接收 sso-client 注销请求,注销所有会话
接下来,我们按照原理来一步步实现 sso 吧!
1、sso-client 拦截未登录请求
java 拦截请求的方式有 servlet、filter、listener 三种方式,我们采用 filter。在 sso-client 中新建 LoginFilter.java 类并实现 Filter 接口,在 doFilter() 方法中加入对未登录用户的拦截
|
|
2、sso-server 拦截未登录请求
拦截从 sso-client 跳转至 sso 认证中心的未登录请求,跳转至登录页面,这个过程与 sso-client 完全一样
3、sso-server 验证用户登录信息
用户在登录页面输入用户名密码,请求登录,sso 认证中心校验用户信息,校验成功,将会话状态标记为 “已登录”
|
|
4、sso-server 创建授权令牌
授权令牌是一串随机字符,以什么样的方式生成都没有关系,只要不重复、不易伪造即可,下面是一个例子
|
|
5、sso-client 取得令牌并校验
sso 认证中心登录后,跳转回子系统并附上令牌,子系统(sso-client)取得令牌,然后去 sso 认证中心校验,在 LoginFilter.java 的 doFilter() 中添加几行
|
|
verify() 方法使用 httpClient 实现,这里仅简略介绍,httpClient 详细使用方法请参考官方文档
|
|
6、sso-server 接收并处理校验令牌请求
用户在 sso 认证中心登录成功后,sso-server 创建授权令牌并存储该令牌,所以,sso-server 对令牌的校验就是去查找这个令牌是否存在以及是否过期,令牌校验成功后 sso-server 将发送校验请求的系统注册到 sso 认证中心(就是存储起来的意思)
令牌与注册系统地址通常存储在 key-value 数据库(如 redis)中,redis 可以为 key 设置有效时间也就是令牌的有效期。redis 运行在内存中,速度非常快,正好 sso-server 不需要持久化任何数据。
令牌与注册系统地址可以用下图描述的结构存储在 redis 中,可能你会问,为什么要存储这些系统的地址?如果不存储,注销的时候就麻烦了,用户向 sso 认证中心提交注销请求,sso 认证中心注销全局会话,但不知道哪些系统用此全局会话建立了自己的局部会话,也不知道要向哪些子系统发送注销请求注销局部会话
![](data:image/svg+xml;utf8,)
7、sso-client 校验令牌成功创建局部会话
令牌校验成功后,sso-client 将当前局部会话标记为 “已登录”,修改 LoginFilter.java,添加几行
|
|
sso-client 还需将当前会话 id 与令牌绑定,表示这个会话的登录状态与令牌相关,此关系可以用 java 的 hashmap 保存,保存的数据用来处理 sso 认证中心发来的注销请求
8、注销过程
用户向子系统发送带有 “logout” 参数的请求(注销请求),sso-client 拦截器拦截该请求,向 sso 认证中心发起注销请求
|
|
sso 认证中心也用同样的方式识别出 sso-client 的请求是注销请求(带有 “logout” 参数),sso 认证中心注销全局会话
|
|
sso 认证中心有一个全局会话的监听器,一旦全局会话注销,将通知所有注册系统注销
|
|
———————————————END——————————————–
我有一个微信公众号,经常会分享一些 Java 技术相关的干货;如果你喜欢我的分享,可以用微信搜索 “Java 团长” 或者 “javatuanzhang” 关注。
作者:凌承一 链接:https://www.cnblogs.com/ywlaker/p/6113927.html 来源:博客园小知
JWT 可以用于单点登录
单点登录是我比较喜欢的一个技术解决方案,一方面他能够提高产品使用的便利性,另一方面他分离了各个应用都需要的登录服务,对性能以及工作量都有好处。
自从上次研究过 JWT 如何应用于会话管理,加之以前的项目中也一直在使用 CAS 这个比较流行的单点登录框架,所以就一直在琢磨如何能够把 JWT 跟单点登录结合起来一起使用,尽量能把两种技术的优势都集成到项目中来。本文介绍我从 CAS 思考得出的 SSO 的实现方案。
前言
其实 CAS 这个方案很好,非常强大,它最新的版本已经集成 JWT 了,所以要是不想自己开发单点登录的服务的话,完全可以考虑使用 CAS。但是我认为,我们在做项目的时候,也许一开始并不需要这么强大的产品,CAS 提供的登录形式有很多,而我们只需要应用其中的一种;
而且它这个框架正是因为强大,所以也会比较复杂,简单上手容易,但是遇到一些特殊的需求,比如我们想在 CAS 里面加入微信登录,那就需要对它的原理以及 API 有比较深入的了解才行。综合考虑,还是弄清楚 CAS 的原理,自己来实现一个基本的 SSO 服务比较放心。
本文的内容需要对 JWT 和 SSO 有一个基本的了解,你可以从这两篇文章来了解 JWT 的用途:
http://www.cnblogs.com/lyzg/p/6067766.html http://www.cnblogs.com/lyzg/p/6028341.html
还可以从下面的资料来了解 SSO 的内容:
https://baike.baidu.com/item/SSO/3451380?fr=aladdin
方案介绍
本文主要是通过时序图的方式来介绍 JWT SSO 的实现原理,具体的技术实现暂时还没有,不过当你理解了这个方案的原理后,你会觉得最终的实现并不会特别复杂,你可以用任意的平台语言来实现它。
下面的时序图,模拟了三个服务,分别是 CAS、系统 A、系统 B,它们分别部署在 http://cas.com,http://systemA.com 和 http://systemB.com;CAS 这个服务用来管理 SSO 的会话;系统 A 和系统 B 代表着实际的业务系统。我从五个场景分别来说明这个 SSO 方案的实现细节。下面先来看第一个。
场景一:用户发起对业务系统的第一次访问,假设他第一次访问的是系统 A 的 some/page 这个页面,它最终成功访问到这个页面的过程是:
图片点击放大看,下同
在这个过程里面,我认为理解的关键点在于:
- 它用到了两个 cookie(jwt 和 sid) 和三次重定向来完成会话的创建和会话的传递;
- jwt 的 cookie 是写在 http://systemA.com 这个域下的,所以每次重定向到 http://systemA.com 的时候,jwt 这个 cookie 只要有就会带过去;
- sid 的 cookie 是写在 http://cas.com 这个域下的,所以每次重定向到 http://cas.com 的时候,sid 这个 cookie 只要有就会带过去;
- 在验证 jwt 的时候,如何知道当前用户已经创建了 sso 的会话?因为 jwt 的 payload 里面存储了之前创建的 sso 会话的 session id,所以当 cas 拿到 jwt,就相当于拿到了 session id,然后用这个 session id 去判断有没有的对应的 session 对象即可。
还要注意的是:CAS 服务里面的 session 属于服务端创建的对象,所以要考虑 session id 唯一性以及 session 共享(假如 CAS 采用集群部署的话)的问题。session id 的唯一性可以通过用户名密码加随机数然后用 hash 算法如 md5 简单处理;session 共享,可以用 memcached 或者 redis 这种专门的支持集群部署的缓存服务器管理 session 来处理。
由于服务端 session 具有生命周期的特点,到期需自动销毁,所以不要自己去写 session 的管理,免得引发其它问题,到 github 里找开源的缓存管理中间件来处理即可。存储 session 对象的时候,只要用 session id 作为 key,session 对象本身作为 value,存入缓存即可。session 对象里面除了 session id,还可以存放登录之后获取的用户信息等业务数据,方便业务系统调用的时候,从 session 里面返回会话数据。
场景二:用户登录之后,继续访问系统 A 的其它页面,如 some/page2,它的处理过程是:
从这一步可以看出,即使登录之后,也要每次跟 CAS 校验 jwt 的有效性以及会话的有效性,其实 jwt 的有效性也可以放在业务系统里面处理的,但是会话的有效性就必须到 CAS 那边才能完成了。当 CAS 拿到 jwt 里面的 session id 之后,就能到 session 缓存服务器里面去验证该 session id 对应的 session 对象是否存在,不存在,就说明会话已经销毁了(退出)。
场景三:用户登录了系统 A 之后,再去访问其他系统如系统 B 的资源,比如系统 B 的 some/page,它最终能访问到系统 B 的 some/page 的流程是:
这个过程的关键在于第一次重定向的时候,它会把 sid 这个 cookie 带回给 CAS 服务器,所以 CAS 服务器能够判断出会话是否已经建立,如果已经建立就跳过登录页的逻辑。
场景四:用户继续访问系统 B 的其它资源,如系统 B 的 some/page2:
这个场景的逻辑跟场景二完全一致。
场景五:退出登录,假如它从系统 B 发起退出,最终的流程是:
最重要的是要清除 sid 的 cookie,jwt 的 cookie 可能业务系统都有创建,所以不可能在退出的时候还挨个去清除那些系统的 cookie,只要 sid 一清除,那么即使那些 jwt 的 cookie 在下次访问的时候还会被传递到业务系统的服务端,由于 jwt 里面的 sid 已经无效,所以最后还是会被重定向到 CAS 登录页进行处理。
方案总结
以上方案两个关键的前提:
- 整个会话管理其实还是基于服务端的 session 来做的,只不过这个 session 只存在于 CAS 服务里面;
- CAS 之所以信任业务系统的 jwt,是因为这个 jwt 是 CAS 签发的,理论上只要认证通过,就可以认为这个 jwt 是合法的。
jwt 本身是不可伪造,不可篡改的,但是不代表非法用户冒充正常用法发起请求,所以常规的几个安全策略在实际项目中都应该使用:
- 使用 https
- 使用 http-only 的 cookie,针对 sid 和 jwt
- 管理好密钥
- 防范 CSRF 攻击。
尤其是 CSRF 攻击形式,很多都是钻代码的漏洞发生的,所以一旦出现 CSRF 漏洞,并且被人利用,那么别人就能用获得的 jwt,冒充正常用户访问所有业务系统,这个安全问题的后果还是很严重的。考虑到这一点,为了在即使有漏洞的情况将损害减至最小,可以在 jwt 里面加入一个系统标识,添加一个验证,只有传过来的 jwt 内的系统标识与发起 jwt 验证请求的服务一致的情况下,才允许验证通过。这样的话,一个非法用户拿到某个系统的 jwt,就不能用来访问其它业务系统了。
在业务系统跟 CAS 发起 attach/validate 请求的时候,也可以在 CAS 端做些处理,因为这个请求,在一次 SSO 过程中,一个系统只应该发一次,所以只要之前已经给这个系统签发过 jwt 了,那么后续 同一系统的 attach/validate 请求都可以忽略掉。
总的来说,这个方案的好处有:
- 完全分布式,跨平台,CAS 以及业务系统均可采用不同的语言来开发;
- 业务系统如系统 A 和系统 B,可实现服务端无状态
- 假如是自己来实现,那么可以轻易的在 CAS 里面集成用户注册服务以及第三方登录服务,如微信登录等。
它的缺陷是:
- 第一次登录某个系统,需要三次重定向(不过可以优化成两次);
- 登录后的后续请求,每次都需要跟 CAS 进行会话验证,所以 CAS 的性能负载会比较大
- 登陆后的后续请求,每次都跟 CAS 交互,也会增加请求响应时间,影响用户体验。
本文小结
本文从理论层面介绍了结合 jwt 来实现 SSO 的方案原理,希望它能帮助一些朋友更好的理解 SSO 以及它的实现方法。本文方案参考自 CAS 的实现流程,你可以从下面这个资料了解 CAS 的单点登录实现过程:
https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html
它的流程跟我这个差别不是特别大,但是从清晰层面来说,我写的还是要更明了一些,所以对比起来阅读,可能理解会更透彻些。
另外,这个方案考虑地不一定很全面,所以要是您发现了其中的问题,还请您帮忙指正,非常感谢。
作者:流云诸葛