单点登录方案与实现总结
单点登录方案与实现总结
何为登录?
众所周知,http是一个无状态协议。何为无状态?举几个有状态协议的例子,譬如TCP协议,需要三次握手,握手完成后双方及建立了连接,可以互相传输数据,传输完成后会断开连接。在譬如websocket,是一个全双工协议,也需要双方建立连接与断开连接。可曾听说过断开http连接?好像没有这回事吧,只有这次http请求200了,还是500报错了这种说法,因为http是无状态的,这次发的http请求与下一次并没有联系,也就不存在断开的概念。
看这样的场景,我需要访问某个网站的属于我的数据了,我发了一个http请求,这次http带有了用户凭据(用户密码),被认证为有权限访问资源,ok,获取到了,那下次再访问这个资源,难道我还需要再输一遍密码么,那也太麻烦了。我怎么样才能让对方知道,我输过密码认证成功是我本人了,就算做我进入“登录状态”了呢?
于是,有了这样一个方案,认证成功后,服务端(对方网站服务器)与客户端(我的浏览器)都记录下同一个值,这个值一般就叫做”JSESSIONID”。如何记录?服务端认证成功,便生成这个值存于其本地,并将其塞入我的Cookie,返回给我,我每次后续的http请求,Cookie都带上这个”JSESSIONID”,服务端找到本地对应的Session信息校验这个Cookie的有效性(真伪/是否到期),有效则返回我的受保护资源。这样,我和目标网站之间就在多个无状态的http请求之间维护出一个登陆状态,靠的就是这个Session值。
然而,这样的方案是有局限性的,Cookie只在同域名下有效,而对方名下有多个网站,都用的一套的用户名密码,我在切换网站时却要重新输入密码,因为域名不同,Cookie失效了,”JSESSIONID”无法带过去了。怎么办?单点登录得出马了。
何为单点登录?
单点登录(Single Sign On),简称为SSO,即在用户登录一次,即可直接获取多个相互信任的应用(服务)的受保护资源,而无需再次进行登录认证。
单点登录方案
单点登录常见的几种方案:
- 同域名
- 共享Session
- CAS
- oauth2
- SAML
- ……
同域名
前面说的,Session存在于服务端,并将对应的”JSESSIONID”存于用户的Cookie。这就带来了两个问题:
- 不同域名,Cookie会失效
- 一台应用登录后,另一台并没有对应的Session
第一个问题好办啊,把所有应用都放在同一个域名下不就可以了么,要知道,Cookie是可以在子域名下共享的,譬如,aaa.com下的Cookie可以在app1.aaa.com何app2.aaa.com下共存,这样,如果多个域名都指向同一台服务器,也没有问题二,那么多域名单点登录问题就解决了。
可是,一般后端应用都不止一台,域名与应用也至少是1:1的关系,这样,问题二的存在还是不能实现单点登录。接下来看第二个方案,使用token的方式。
我们判断用户认证信息不再直接通过Session对象,在Main模块,也就是认证模块,会生成一个Token放到Cookie中,其他子应用则负责解析这个Token,如果是用的Spring Security,一般这个工作会交给RememberMeAuthenticationFilter
去做,它会根据配置的token key自动解析出对应token值,并生成对应用户的局部Session。
这里的token可以采用JWT,携带一部分用户信息,减轻服务器查询用户信息的压力,可以使用JWS保证JWT的安全性。
单点登出
由于用户登录信息存储在Cookie,所以直接Set-Cookie对应字段为空即可。
共享Session
应用不止一台,那么在某一台登录,生成的Session也同步到其他的应用,可行么?当然可以,Session一般是存在内存当中的,只要把它从内存迁移出来,放到一个统一的地方,所有应用都从这一处来获取Session,与Cookie带过来的JSESSIONID信息对比不久可以了么。
Spring已经造好了轮子,Spring-Session就是一个共享Session的组件,可以把Session存到Redis中去,就可以实现一处生成Session多处使用了。
这样,前面提到的两个问题就都解决了。
但是,如果是不同域名下的应用呢,二级域名也不一样,比如app1.aaa.com和app2.bbb.com,如何实现单点登录?目前来说有下面几种方案。有很多种协议,比如Token,CAS,OpenID,OAuth2,SAML等等。
单点登出
清楚用户对应的Session对象即可。
CAS
CAS是一套企业范围的单点登录方案,即单点登录应用与受信任应用都是属于一个企业的,登录范围也是在该企业下的所有应用。
相关概念
CAS协议下,存在几个概念先明确一下。
User
这个好理解,就是受保护资源的所有者。
CAS Server
负责验证用户并授予用户访问应用程序的权限。一般只存在一个CAS服务器。
CAS Client
一般会存在多个CAS客户端。其负责保护所在的CAS应用,并从CAS服务器检索授权用户的身份信息。
TGC(Ticket-granting cookie)
存在于用户与CAS服务器之间的Cookie,用于存放用户认证凭证。
TGT(Ticket Granting Ticket)
一个Cookie值,存于TGC中,用于维持用户与CAS服务器之间的会话,一般叫这个会话为SSO session。
ST(Service Ticket)
用户可以访问某个受保护服务的凭据(直译过来的话是服务票据)。这个ST是生成TGT之后,由CAS Server颁发的。
关键逻辑
首先只维持用户与CAS Server之间的登录状态(SSO Session)。
然后,由于子应用都信任于CAS Server,所以CAS Server颁发一个TGT和一个ST给用户,TGT给用户用来维持其与CAS之间的登录状态。
用户拿到了ST,使用ST去访问子应用(app1,app2),子应用去与关联的CAS Server核验该ST是否有效,如果有效,从CAS Server获取用户身份信息,并于用户生成局部Session,设置自己域名的JSESSIONID。
用户Cookie使用子应用的JSESSION访问子应用受保护资源。
整个流程时序图如下:
流程图获取有些复杂,需要耐心地一步步过一遍,有个印象。其实实现起来并非那么麻烦,有现成的CAS框架,比如Yale大学发起的一个单点登录项目,全名为Central Authentication Service,也简称CAS。其中提供了多种单点登录协议的实现,CAS协议就是其中一种。具体可见https://www.apereo.org/projects/cas。
或者,我们可以借鉴一下CAS的逻辑,实现一套自己的CAS登录逻辑。关键点,就是流程图中提到的几个EndPoint:
GET {casServer}/cas/login?service=app1
从受保护app1重定向到CAS服务器的登录界面。注意,service内容是URL encoded的。
POST {casServer}/cas/login
用于CAS服务器作为SSO登录的post接口。
GET {casClient}?ticket=ST-123456
可以在CAS客户端定义一个Filter,去捕捉当前URI是否存在ST,如果存在,则到CAS Server去验证它。
GET {casServer}/serviceValidate?service=app1&ticket=ST-123456
CAS服务器通过这个endPoint来验证ST是否有效,有效则通过XML-Document方式返回用户信息给CAS客户端。
这个ST是可以存储在LocalStorage中的,可以绕过跨域问题。
这三个实现的差不多的话,基本的CAS逻辑也就完成了。
单点登出
CAS单点登出相较于之前的略微麻烦些,由于用户在每个CAS Client所处应用上都有局部Session以及对应Cookie,所以需要CAS Server调用所有CAS Client提供的登出API,实现全部登出。
OAuth2
关于OAuth2协议,详细的介绍可以参考https://datatracker.ietf.org/doc/html/rfc6749
相关概念
OAuth2下下有以下几个角色:
resource owner(资源所有者)
官方定义为能够授予对受保护资源的访问权限的实体,这个实体不一定是指代人,或许他也是台后端服务器。如果resource owner指代的是人的话,又有个别名,叫做end-user。
resource server(资源服务器)
受保护资源所在的服务器端,它能够接收用户传过来的access_token,返回受保护资源。
client(客户端)
client是经过了resource owner授权的,用来访问resource server的应用。其持有的是用户的授权信息。
authorization server(授权服务器)
验证用户凭证成功并且接收用户授权后,authorization server将向client发放access_token。
关键逻辑
OAuth2本身其实不是用来做单点登录的,而是用于将用户权限授予客户端。用它也可以实现单点登录,将多个子应用均作为受信任的resource server,只要用户拥有了access_token,就可以访问所有子应用资源。
OAuth2有几种模式,包括授权码模式(Authorization Code Grant),隐式授权模式(Implicit Grant),资源所有者密码模式(Resource Owner Password Credentials Grant),以及客户端凭证模式(Client Credentials Grant)。这里简单讲下授权码模式,也是最复杂、使用度最高的一种模式。大致流程如下:
简单来说:
client不属于我,不能直接访问我受Authorization Server保护的资源。
于是,client把它的身份信息(e.g. client-id,client-secret etc.)在authorization server进行了注册。
并且在我第一次使用此client时,client会向authorization server发送认证请求。
authorization server一般会通过重定向的方式,把我的浏览器重定向到它的登录页。我在这个登录页提交我的身份凭据(用户名、密码)。
authorization server通过了认证,把我的页面重定向到授权页,我可以选择授予client哪些权限(头像、联系方式等)。
我向authorization server提交了授权请求,授权服务器给我一个授权码,并重定向到client的认证url。
client获取到了授权码,向Authorization Server索要access_token。
client获得并存储access_token,使用它来访问resource server。
整个过程就是resource owner把受保护权限赋予client的过程。
获得了access_token,也就可以用来访问所有的子应用了。这里token并非存储在Cookie,而是一般存储在浏览器Local Storage中,所以并无跨域问题。
resource server一般会读取authorization server库中存储的token,看是否过期来验证token,亦或者是token使用JWS,自带过期时间,且共要加密与签名的存在保证了JWS不可伪造,可以直接判断token是否有效。
以下时序图引用自理解 OAuth 2.0 认证流程
单点登出
相较于CAS,OAuth2的单点登出更为麻烦,因为client一般都是第三方的,client会注册到authorization server,但authorization server并无法去调用client。如果access_token被持久化了倒还好,将用户对应持久层的token置为失效即可,下次验证token即不通过。但如果并没有持久化,比如使用的是JWS,就只能由客户端自己去清除当前client的access_token,也无法访问到其他所有client。所以比较棘手,只能等对应的access_token自己失效。
与CAS对比
CAS下,受保护资源是属于客户端的。用户访问客户端,客户端通过CAS服务器来验证该用户是否有权访问自己的资源。而在OAuth2下,受保护资源是属于服务端的。用户需要让客户端知道,这个客户端是否有权访问用户位于服务端的数据。
这是CAS的大致流程,CAS Client用于保护所在服务器的资源,一般也是位于服务端。
而OAuth2下,Client是独立的,它可以在手机上,浏览器上,PC软件上(经常用到的,用网易账号登录有道云笔记pc端,这里有道云笔记软件就是client,访问的网易云authorization server,获取我在网易云的头像等信息),又或者是另一个独立的后端服务器上,纯纯的客户端。
遗憾的是,OAuth2是一个授权协议,client最终拿到的,是用户的权限范围,而非用户本身的详细信息。如果client就需要用户详细信息了,该怎么办?
于是,在OAuth2的基础上,又出现了另一个协议:OIDC。
OIDC(OpenID Connection)
看起来好像很高大上,其实就是在OAuth2的基础上再封装了一层,可以理解为加强版OAuth2,使OAuth2具备了认证能力。其中新增了几个概念:
相关概念
OpenID Provider
负责签发ID token。也就是authorization server。
ID token
一个JWT格式的字符串,存储了用户的一些基本信息.
end-user
不同于OAuth2的resource owner,这里具体到了,资源所有者就是人。
userinfo endpoint
用户信息端点,位于authorization server,用于返回用户更加详细的认证信息。
scope标准化
scope中必须带有openid。
Claim
终端用户信息字段。
单点登出
同OAuth2的问题,token未持久化,且分布在用户代理,难以在authorization server统一清除,需要一些额外的开发量来实现。
总结
大概列举了几种实现SSO的方式,各有千秋,还是要具体情况具体分析,适合业务场景的才是最好的方案。
方案 | 优点 | 缺点 |
---|---|---|
同域名 | 开发成本低,理解难度小,维护成本低。 | 分布式场景支持性较差,很多应用非同域名。 存放于Cookie,安全性低。 用户如果关闭Cookie则直接无法使用。 |
共享Session | 降低服务器内存开销。分布式支持性较好。 | 维护成本较高,需要管理缓存集群。 |
CAS | 分布式架构下支持性很好,适合企业内部应用间互相认证。 | 架构较为复杂,开发成本较高。 |
OAuth2/OIDC | 分布式架构下支持性很好,基于JWS可减轻服务器压力。 适用于需要将企业内部应用资源开发给第三方时使用。 |
架构较为复杂,开发成本高,单点退出比较难搞。 |
还有其他的SSO Protocol,例如SAML,还没研究过,所以暂时不总结了。。。以上内容大多是最近学习的内容,包含了不少自己的理解,所以可能会有些错误,实际开发时还需要多翻阅官方手册。