单点登录(SSO)是一种身份验证解决方案,可让用户通过一次性用户身份验证登录多个应用程序和网站。关于单点登录的设计理念,有个视频讲的很清楚。
https://www.youtube.com/watch?v=Bv6NZlqqn48
实现SSO的方式有很多种,比如Kerberos、SAML、OAuth2、JWT、CAS、LDAP等。
Kerberos
同样有个讲的不错的视频:https://www.youtube.com/watch?v=5N242XcKAsM。下文的图片均摘自视频。还可以参考Microsoft官方文档:Microsoft Kerberos - Win32 apps | Microsoft Learn
Kerberos协议定义了客户端如何与网络身份验证服务交互。 客户端从 Kerberos 密钥分发中心 (Key Distribution Center,KDC) 获取票证,并在建立连接时向服务器出示这些票证。KDC是Kerberos的核心,它包含两个服务器:身份认证服务器(确认是已知用户发出的请求并颁发票据授予票证)、票据授予服务器(确认是向已知服务发起请求并颁发服务票证)。Kerberos的两大特点,密码从未在网络中直接传输,加密密钥也不会直接进行交换。
1. User to AS
用户向AS(Authentication Server)发起请求,表明要访问什么样的服务。请求中包含了User名称/ID、要访问的Service、IP地址等信息。AS中存有用户信息表,根据收到的User名称/ID判断是否为已知用户。
2. AS to User
如果AS判断是已知用户,会返回两条消息。第一条包含TGS(Ticket Granting Server)的名称/ID,时间戳和消息的生命周期。第二条是生成的TGT(Ticket Granting Ticket,票证授予票证)。另外,这两条消息的最后都加入了TGS Session Key,授予票证服务器的密钥是随机生成的的。第一条消息,根据用户名/ID找到对应的Client Secret Key加密信息。第二条消息,用TGS的密钥TGS Secret Key进行加密。
用户输入自己的密码,然后通过哈希算法会生成Client Secret Key,用于解密第一条消息。这条消息就是用于验证用户身份,只有密码正确才能成功解密。然后用户就能获取到TGS Name/ID和TGS Session Key。但是由于用户没有TGS Secret Key,所以无法对第二条消息解密。
3. User to TGS
用户生成两条消息,第一条是明文消息,包含想要访问的Service名称和要请求的ticket生命周期。第二条消息是用户身份认证器,包含User名称/ID和创建的时间戳。另外会根据上一步Client Secret Key解密后获取的TGS Session Key对第二条消息进行加密。然后和上一步中未能解密的消息一起,发送给TGS。
TGS根据明文消息中的Service名称/ID,在自己的TGS列表中查找对应的Service Secret Key。
如果找到了对应的Key就来解密TGT消息。然后从消息中得到TGS Session Key,并用于解密User Authenticator身份验证器。
然后TGS会进行验证1. TGT中的User Name/ID是否和User Authenticator匹配。2.时间戳是否匹配,一般时间戳差异最多是两分钟。如果匹配成功,将User Authenticator中的信息写入到TGS缓存中。
4. TGS to User
然后TGS会发送两条消息,第一条是包含要访问的Service Name/ID、时间戳、生命周期。第二条是关于Service Ticket。同样TGS也会给两条消息的后面加上随机生成的Service Session Key。然后第一条用TGS Session Key加密,第二条用Service Secret Key加密。然后发送给用户
5. User to Service
用户接收上述消息后,用TGS Session Key(在AS to User这步中获取的)进行第一条消息的解密,然后得到其中的Service Session Key。再用这个去加密生成User Authenticator。将Service Ticket和User Authenticator一起发送给Service
6. Service to User
Service用Service Secret Key解密Service Ticket消息得到Service Session Key,然后再用它解密User Authenticator。将两个消息中的User Name/ID进行比较,如果一致进而判断时间戳是否符合。
如果通过校验,写入Service的缓存中。
然后生成消息发给User。User解密后判断Service Name/ID是不是自己想要访问的服务。
另外,User也会在自己的缓存中存储一份Service Ticket,以便后续访问。
CAS
CAS (Central Authentication Service) 最初由耶鲁大学于2001年开发。是面向Web的单点登录SSO解决方案。后来CAS项目被纳入Apereo Foundation管理,更名为Apereo CAS。参考Apereo官方文档:CAS - Architecture
CAS的架构如下。CAS 客户端是任何支持 CAS 的应用程序,CAS服务器负责颁发和验证票据来校验用户身份,进而授予CAS客户端的访问权限。
CAS流程参考:CAS - CAS Protocol。流程图片如下。
用户向目标系统发起访问,会重定向到CAS服务器,如果此时用户没有SSO Session,CAS服务器会向客户发送登录表单,输入用户名密码等。如果验证成功,CAS服务器会创建SSO Session(TGC)并给浏览器发送Cookie(Cookie中包含TGT票据这个票据就是SSO Session的seesion key)和ST票据。浏览器带着这个ST票据去访问应用程序,应用程序则向CAS服务器发送请求,校验ST票据。如果校验成功,应用程序再返回浏览器一个Session Cookie。下次再访问同一个应用,带着这个Session Cookie就可以免登录。如果访问的是不同的应用,则需要带着TGT Cookie。
JWT
图片参考视频:https://www.youtube.com/watch?v=soGRyl9ztjI
我们访问服务的A功能时,服务会要求我们输入用户名密码等信息来校验身份,那么当访问B功能时,服务会记住我们是谁,而无需再次登录。这就需要记录会话。记录会话的方式之一就是使用token。最常见的token包括Session token和JWT(JSON Web Token)
Session token的设计理念是,等我们向服务器认证后,服务器创建一个Session,并维护一个Session表记录了相关信息,将对应的Session ID返回给客户端。下次再访问时,客户端将这个Session ID作为请求的一部分发送给服务器,服务器从Session表中查看是否存在对应的信息来判断能否通过认证。而客户端为了能存储这个Session ID,将它放到了Cookie中。
这种Session ID+Cookie的形式是最常见的,但是它也存在了一些问题。首先它假设了Web APP只有一个。而现在负载均衡的架构下,客户端访问a App,然后a App存储了Session,但是这个Session不能用于b App。所以有些解决方案是用Redis缓存来保存并共享这些会话。但是这个也有个缺点,一旦Redis出现单点故障,所有App的会话都会出现问题。
Session ID的形式是在服务器中存储用户详细信息,并给用户一个相应的Session ID,当服务器多了可能会出现问题。那么反过来思考,用户是单一的,能不能用户的详细信息由服务器来发放后在用户端存储会话,下次登录时带着这份详细信息的记录。但是这样的方案也存在一个问题,信息存储在客户端会不会被恶意篡改。一个解决方案是在服务器发放的用户信息会话中加入签名,下次登录时只校验签名。这就是JWT的设计思路。
Session token更像是Reference token,指向了某个Session ID对应的对象。而JWT更像是value token,包含了具体的值。jwt的生成和调试网站:JSON Web Tokens - jwt.io
JWT分为三个部分,Header、Payload、SIGNATURE。用户信息,即具体的值value,就是payload部分。上面提到JWT是需要签名来防篡改的。Header部分指定了签名用的算法,而签名部分SIGNATURE则是只有服务器能够计算,由它计算后进行赋值。这一点体现在下面的"your-256-bit-secret"只有服务器才有。
用户想要更改前面Header和Payload的值很容易,这些本来就是透明的。但是如果想要更改SIGNATURE的值,需要知道secret-key密钥。
客户端和服务器的JWT交互流程如下。用户发送身份验证所需的信息,服务器验证成功后生成JWT给客户端。所以需要注意:JWT不是用于身份认证的,而是用于识别用户是谁的(即,授权),是身份认证成功后才会产生的。客户端可以本地或者通过Cookie存储JWT,后续请求中在HTTP头部带入该值。然后服务器校验签名。
Validate这个签名验证过程也很简单,就是对JWT的Header和Payload部分,根据secret key进行计算,得到一个签名值,然后和SIGNATURE比较,这两个值是否相等。
这样也可以看到JWT的一个缺点,对于JWT的窃取没有很好的办法。一般对于Session会设置过气时间,但是由于JWT的值都在客户端存储,即使payload中设置过期时间,这个时间也可以被篡改。
RFC 7519 是定义 JSON Web Token (JWT) 的标准。它详细描述了 JWT 的结构、使用方法以及相关的安全性考虑。参见:RFC 7519 - JSON Web Token (JWT)
SAML
图片参考视频:https://www.youtube.com/watch?v=SvppXbpv-5k
SAML(Security Assertion Markup Language,安全断言标记语言)是身份验证和授权数据交换的标准。SAML 规范定义了三个角色:用户、IDP(IDENTITY PROVIDER,身份提供者)、SP(SERVICE PROVIDER服务提供商)。IDP和SP之间是信任关系。
用户想要访问服务,会被重定向到IDP,IDP验证成功后,IDP生成SAML ASSERTION断言(包含了用户身份认证的信息)。然后将这个断言传给SP,SP校验该断言的有效性和真实性(例如检查签名、时间戳等)。
OAuth2
图片参考视频:https://www.youtube.com/watch?v=ZV5yTm4pT8g
一个场景:我们手机拍的照片会上传到SnapStore中进行存储。如果我们想要打印这些照片,再将这些照片上传到打印机中就很麻烦。通过OAuth2,打印机向SnapStore申请授权,进而访问这些照片,而无需用户再次上传。通过OAuth2授权的特点是,无需知道SnapStore的密码等登录凭证,就可以共享用户授权。
具体的流程如下。用户让打印机打印照片,打印机向SnapStore申请授权(发送客户端ID、范围;表示作为资源所有者向授权服务器请求的访问级别)。SnapStore向用户发送请求权限的对话框,如果用户同意,SnapStore则向打印机发送authorizationcode。打印机带着这个code向SnapStore发送clientId和clientsecret(这个密钥是仅在打印机和SnapStore之间共享的私钥)。如果SnapStore验证成功,打印机就可以访问相关的资源。
这个流程保证了SnapStore的登录凭据并不会暴露给打印机,并且打印机只能访问用户指定的scope范围的资源。
OAuth2在很多授权场景下都有应用,一般登录网站或App时,可以选择用github、微信、QQ等第三方登录方式,这种就属于OAuth2。
OAuth2只做了授权,提供了一个应用程序的访问密钥。但它不包含登录的相关内容。OpenID Connect又称OIDC,位于OAuth2之上,记录了登录相关信息。OIDC记录了用户名、用户ID、登录时间、有效期等内容,更像是身份提供者。