分布式鉴权技术
OAUTH2
基本角色定义
- 授权服务器 - 验证身份并颁发令牌的服务器(我上面代码示例中的 OAuth 服务器)
- 资源服务器 - 提供 API 资源的服务器
- 客户端应用 - 访问 API 的应用,可以是:
- 第一方应用 - 您公司自己的应用
- 第二方应用 - 业务合作伙伴的应用
- 第三方应用 - 外部开发者的应用
token 类型
- access_token (默认有效期 24 小时)
- refresh_token(默认有效期 30 自然日,每次刷新 access_token 的操作可自动刷新 refresh_token 有效期的起始计算时间)。
客户端凭证流程(适用于服务器到服务器通信)
1 |
|
这种流程中:
- 获取令牌方: 第一方或第二方应用直接获取令牌
- 使用令牌方: 同样的应用使用令牌调用API
- 无用户参与: 整个过程无需最终用户授权
1 |
|
授权码流程(适用于有用户参与的场景)
1 |
|
这里面有两次重定向:
第一次:重定向到授权服务器。用户是带着一个 param 去访问 authserver 的,在我们常见的场景里线索通跳到 sso 是第一次重定向,参数里带有 https://app.example.com/callback 就是授权完以后再回到线索通的一个 url。
- 当用户尝试访问需要授权的功能时,应用生成一个授权请求 URL
- 这个 URL 包含 redirect_uri 参数(应用事先在授权服务器注册的 URL)
- 应用通过 HTTP 302重定向或者前端跳转,将用户浏览器导向授权服务器
1 |
|
第二次:重定向回应用,携带授权码(code)。
- 用户在授权服务器完成身份验证并授予权限后
- 授权服务器使用之前请求中的redirect_uri,将用户浏览器重定向回应用
- 重定向 URL 中附加授权码(code)作为查询参数
1 |
|
- 授权服务器只会重定向到预先注册的 URI,防止授权码被重定向到恶意网站
- 防止授权码被重定向到恶意网站
1 |
|
刷新令牌流程 (Refresh Token Flow)
1 |
|
SSO
精确的SSO登录流程 (结合联邦认证)
这个流程描述了当用户首次访问系统,且SSO认证中心本身也需要借助第三方(如QQ)来完成认证的完整过程。
核心参与者
- 用户 (浏览器): 用户的设备。
- 系统A (应用/Service Provider): 用户想要访问的业务系统,例如 app-a.com。
- SSO服务器 (认证中心/Identity Provider): 统一的身份认证服务,例如 sso.com。
- QQ (外部认证方/External IdP): 提供身份信息的第三方,例如 qq.com。
流程步骤
访问应用: 用户在浏览器中访问系统A (
app-a.com
) 的受保护页面。系统A检查发现浏览器请求中没有自己的登录凭证(app_a_cookie
),判定用户未登录。重定向至SSO: 系统A将用户的浏览器重定向到SSO服务器的登录地址,并在URL中附带自己的身份和期望的回调地址。
sso.com/login?service=https://app-a.com/callback
SSO检查并再次重定向: SSO服务器检查发现浏览器请求中也没有SSO的全局登录凭证(
sso_cookie
),判定用户在认证中心也未登录。此时,SSO服务器呈现一个登录页面,上面有多种登录选项(如:用户名密码、使用QQ登录)。用户选择QQ登录: 用户点击“使用QQ登录”按钮。SSO服务器将浏览器重定向到QQ的授权页面,并附带自己的客户端ID和预设的回调地址(指向SSO自己)。
qq.com/auth?client_id=sso_client_id&redirect_uri=https://sso.com/auth/qq/callback
QQ认证与授权: 用户在QQ的页面上输入账号密码,并同意授权SSO服务器获取其基本信息。
QQ回调SSO: QQ认证成功后,将浏览器重定向回第4步中指定的SSO回调地址,并在URL中附带一个一次性的
authorization_code
。sso.com/auth/qq/callback?code=qq_auth_code_123
SSO后台换取用户信息: SSO服务器的后端收到
authorization_code
后,向QQ的API服务器发起一个安全的后台HTTP请求,用code
换取access_token
,再用access_token
获取用户的唯一标识(openid
)等信息。SSO建立全局会话: SSO服务器确认了用户的身份,此时:
- 在SSO域 (
sso.com
) 下为浏览器种下全局登录凭证(sso_cookie
)。 - 这标志着用户已经在单点登录系统中完成了认证。
- 在SSO域 (
SSO携带Ticket重定向回系统A:SSO服务器现在将浏览器重定向回系统A在第2步中提供的回调地址,并在URL中附带一个一次性票据 (Ticket)。在现实中还会有多余的一步,首先返回一个 uid列表(如果一个qq下有多个 uid的话),让用户选择 uid 以后,再根据 uid 返回一个独一无二的 ticketA(只绑定 uid)。
app-a.com/callback?ticket=ST-xyz-789
系统A后台验证Ticket: 系统A的后端收到
ticket
后,向SSO服务器的验证接口发起一个安全的后台HTTP请求来核销这个ticket
。SSO验证并返回用户信息: SSO服务器验证
ticket
有效(且是发给系统A的),然后立即销毁该ticket
防止重放攻击。验证成功后,向系统A返回该用户的身份信息(如统一的uid
、role
等)。系统A建立本地会话: 系统A收到了确认信息,信任了该用户。它会在自己的域 (
app-a.com
) 下为浏览器种下应用自身的登录凭证(app_a_cookie
),并最终将用户带到他最初想访问的页面。
至此,登录流程完成。如果用户再去访问已接入SSO的系统B,流程会大大简化:他会被重定向到SSO(步骤2),SSO检测到sso_cookie
已存在(步骤8),会直接跳过登录和QQ认证,立即执行步骤9,发放一个新票据给系统B,实现无感知的“单点登录”。
1 |
|
在这个流程里,每个系统首先校验本域下的 cookie,每个服务都一开始只给一个一次性的凭证-一般在url 的query param里附带,直接回调回主浏览器,由浏览器主动重定向回调回上一个系统的回调地址(由sso 到外部系统。),然后外部系统去找发放一次性凭证的服务(先是QQ,然后是SSO)获取凭证代表的具体信息(如用户的 OpenId),然后在本地建立一个会话本域种下 cookie 凭证,回调回上一个系统,上一个系统携带回调 url 里的 query param 里的凭证,访问这个系统的会话,获取信息。
如果有层级的多层调用,回调url也要逐层嵌套和解套。
每一个本域的 cookie 是为了下次访问本系统的时候,不再问上游系统,而直接发放一次性凭证用的。回调地址也应该准备为可以从任意地址重定向到 SSO,从 SSO 又重定向回当前页面。
- 回调地址如果有可能,尽量从 Referer 头获取。
- 如果有可能,回调地址里面一定要去掉 sso_ticket 这种sso 回调才会添加的附加参数。
- ticket 和 token 最好都和账号、回调 uri 绑定。
- 要注意 ticket、uri、token 乱串的情况。如果发生各种绑定出错的情况,尽量使用:
- 已存在的 token 解析出一个登录账号,然后重定向到 sso 服务,因为 sso 域下的 cookie 已存在,又会重定向回这个登录账号在当前账号下的页面。即
/9/account
已经登录了,浏览器输入了一个/10/account
,则需要靠重定向到 sso,再重定向回来/9/account
。如果给 sso 的重定向uri是/10/account
则可能产生无限循环。正确的解法: - 尝试给出一个和 sso_token 匹配的回调地址再登录。有些 sso 系统不支持。因为 cookie 可能账户绑定关系是一对多的,没人知道具体原来使用了哪个账户(可能一个 token绑定了7、8、9)。
- 直接清除 sso_token 的 cookie。
- 已存在的 token 解析出一个登录账号,然后重定向到 sso 服务,因为 sso 域下的 cookie 已存在,又会重定向回这个登录账号在当前账号下的页面。即
- 注意cookie的几个属性:
- domain:如果不设置就是默认当前的域(包括子域):d2ksr8gxr5kl2o(子域名).cloudfront(主域名).net(顶级域名)
- path:如果不设置则为当前路径的父,所以最好设置为/
- MaxAge/Expires:旧版本协议用 Expires,新版本用MaxAge
- HttpOnly:当设置为true时,表示该Cookie只能通过HTTP/HTTPS协议访问。这种设置可以防止JavaScript通过document.cookie访问该Cookie,从而降低XSS(跨站脚本)攻击的风险。这是Web安全的最佳实践之一,特别适用于存储敏感信息(如认证令牌)的Cookie
SSO登出流程 (Logout)
SSO的核心理念是“一次登录,处处通行”。相应地,登出的核心理念应该是“一次登出,处处失效”,这被称为单点登出 (Single Logout, SLO)。
如果只清除了当前应用(系统A)的登录状态,而SSO认证中心的全局会话依然存在,那么用户刷新页面后,系统A会再次将他重定向到SSO,SSO发现全局会话还在,会立刻又把他自动登录回系统A。这就导致了用户无法真正登出的尴尬局面。
因此,正确的登出流程必须由SSO认证中心来协调。也因此,中间服务不要缓存 sso_token,因为不一定能感知到这个 sso_token 被他处 invalidate 了。
流程步骤:
用户发起登出: 用户在系统A的界面上点击“登出”按钮。
系统A清理本地会话: 系统A的后端接收到登出请求后,首先执行本地登出操作。这包括:
- 销毁系统A自身的会话 (Session)。
- 清除种在
app-a.com
域下的本地登录凭证(app_a_cookie
)-这一步需要重新使用 Set-Cookie header 设置一个过期的 cookie。
重定向至SSO进行全局登出: 清理完本地会话后,系统A必须将用户的浏览器重定向到SSO服务器的登出地址。通常还会附带一个参数,告诉SSO登出后应该跳转回哪里。注意这个参数是靠重放 cookie 而不是携带参数来实现的。
sso.com/logout?post_logout_redirect_uri=https://app-a.com/login
SSO执行全局登出: SSO服务器接收到请求后,执行全局登出操作:
- 销毁SSO的中央认证会话。
- 清除种在
sso.com
域下的全局登录凭证(sso_cookie
)。
SSO通知所有相关应用登出 (关键步骤): 这是实现单点登出 (SLO) 的核心。SSO服务器会查找在此次全局会话期间,该用户都登录了哪些应用(例如系统A、系统B、系统C)。然后,SSO服务器会从后端向所有这些应用的预设登出接口发送登出通知 (Logout Notification)。
- 这个通知通常是一个包含了
logout_token
的HTTP POST请求,应用收到后需要验证这个token
的合法性。
- 这个通知通常是一个包含了
其他应用清理本地会话: 系统B、系统C等其他应用收到来自SSO的登出通知后,执行与步骤2相同的本地登出操作,清理各自的会话和Cookie。
最终跳转: 在SSO服务器完成所有登出操作后,它会将用户的浏览器重定向到第3步中指定的
post_logout_redirect_uri
地址,通常是应用的登录页面。用户此时会看到系统A的登录页,表示已成功登出。
至此,用户在整个SSO体系中的所有会话均已失效,实现了彻底的“单点登出”。
1 |
|