乐趣区

前后端鉴权二三事

DevUI 是一支兼具设计视角和工程视角的团队,服务于华为云 DevCloud 平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng 组件库:ng-devui(欢迎 Star)
官方交流群:添加 DevUI 小助手(微信号:devui-official)进群
DevUIHelper 插件:DevUIHelper-LSP(欢迎 Star)

引言

前后端鉴权是一个很大的话题,不同组织的鉴权方式各不相同,甚至对同一协议的业务实现也可能相去甚远。本文尝试从认证与授权两个维度来描述标题中的鉴权,大部分篇幅还是偏认证。文章主要包含三部分内容:区分认证与授权、常见的认证及授权方式和企业应用中常见的单点登录(SSO)方案。

1 认证与授权

首先,我们来简单看一下认证与授权,并理清楚两者之间的区别。

认证(Authentication)

认证涉及一方应用和一方用户,用于描述用户在该应用下的身份。认证可以简单理解为登录,以此确认你是一个合法的用户。比如说掘金必须要登录才能点赞、收藏。

授权(Authorisation)

授权涉及两方应用和一方用户,用于描述第三方应用有哪些操作权限。

带入场景区分认证与授权

我们分别举三个例子来说明三种情况让大家对认证和授权的关系有更好的理解:只认证不授权、即认证又授权、不认证只授权。

(1)只认证不授权

上面提到的使用掘金账号登录掘金就是只认证不授权的场景,此时掘金只知道你是哪个用户,但是不涉及到授权的操作。

(2)即认证又授权

同样是登录掘金,我们可以不使用在掘金注册的账号和密码登录,而选择第三方应用登录,比如说 github 账号。此时会弹出 github 的登录页面,如果你在此页面输入账号和密码进行登录,则相当于默认授权给掘金获取你的 github 的头像和账号名。在这个过程中即完成了认证(合法用户)又完成了授权(你允许掘金从 github 获取你的信息)。

(3)不认证只授权

以某外卖小程序为例,在你第一次进入外卖小程序的时候小程序会弹框请求获取你的个人信息,此时相当于上面提到的即认证又授权。你同意以后就相当于使用微信账号登录,但是此时外卖小程序获取到的你的信息不包括你的手机号。当你要下单点击提交的时候,小程序再次发起请求,要获取你微信绑定的手机号,此时发生的动作就是不认证只授权。

2 有哪些常用的认证和授权方式?

一旦涉及认证,必须要考虑的一个问题就是状态管理。所谓的状态管理就是说我们在一个网站进行登录之后的一段时间里,不希望每次访问它都需要重新登录,所以应用开发者必须要考虑怎么样保持用户的登录状态以及决定何时失效。而这个过程需要前后端通力合作来完成。下面介绍几种常见的认证和授权方式。

Session-Cookie 认证

(1)流程

Session-Cookie 的认证流程如下:用户先使用用户名和密码登录,登录完成后后端将用户信息存在 session 中,把 sessionId 写到前端的 cookie 中,后面每次操作带着 cookie 去后端,只要后端判断 sessionId 没问题且没过期就不需要再次登录。

使用这种方式进行认证,开发者可能面临的主要问题如下:

  • cookie 安全性问题,攻击者可以通过 xss 获取 cookie 中的 sessinId,使用 httpOnly 在一定程度上提高安全性
  • cookie 不能跨域传输
  • session 存储在服务器中,所以 session 过多会耗费较大服务器资源
  • 分布式下 session 共享问题

Token 认证

与上面的 Session-Cookie 机制不同的地方在于,基于 token 的用户认证是一种服务端无状态的认证方式,服务端可以不用存放 token 数据,但是服务器可以验证 token 的合法性和有效性。使用 token 进行认证的方式这里主要介绍两种:SAML 和 JWT.

SAML (Security Assertion Markup Language)

SAML 的流程如下:

  • 未登录的用户通过浏览器访问资源网站(Service Provider,简称 SP)
  • SP 发现用户未登录,将页面重定向至 IdP(Identity Provider)
  • IdP 验证请求无误后,提供表单让用户进行登录
  • 用户登录成功后,IdP 生成并发送 SAML token (一个很大的 XML 对象) 给 SP
  • SP 对 token 进行验证,解析获取用户信息,允许用户访问相关资源

针对上面的流程补充两点信息:

(1)SP 是如何验证 token 的合法性?

比如是否有可能 token 在 IDP 到 SP 的过程中被人劫持并修改了内容?

答案是:没有可能。因为 IDP 返回给 SP 的 token 使用 IDP 的私钥进行了签名,而通过私钥签名后的信息可以通过对应的公钥进行验证。

(2)SP 如何判断 token 是否过期?

SAML token 携带了 token 过期时间的信息。

(3)生成的 SAML token 是托管在 SP 还是前端?

如果是托管在 SP,那么又要引入 session 机制,如果托管在前端,那么前端需要存储并且每次传递 SAML token,但是 SAML token 大小又比较大,耗费传输资源。

答案是:都可以。放在前端的话需要前端通过单独的 ajax 请求获取 token 并存储在 localStorage 或者其他的本地存储中。如果是托管在 SP,那么就像上面说的,引入 session,前端只掌握 sessionId,这样的话 token 机制其实就退化成了上面提到的 session-cookie 机制。

JWT(JSON Web Token)

关于 JWT 的文章有很多,这里不再赘述,相关信息可以参考阮一峰老师的入门文章:JSON Web Token 入门教程(预计阅读时间:2mins)

简言之,JWT 就是一种在用户登录后生成 token 并把 token 放在前端,后端不需要维护用户的状态信息但是可以验证 token 有效性的认证及状态管理方式。

文章里已经有的内容这里不过多探讨,想聊一聊的是在此基础之上延伸出的一个问题:

JWT 用于签名和验证签名的 secret 对于所有人来说都是一样的吗?

如果一样的则存在比较大的安全隐患,一旦泄露,所有 JWT 都可能会被破解。如果不一样,那么同样需要在服务器端维护每一个人对应的 secret 信息,这样的话和服务器端维护 session 信息又有什么区别呢?

从 JWT 官方 Introduction 的介绍文档中看到这样一句话:

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

也就是说,secret 可以用服务器私钥。如果这样的话,对于所有用户都是一样的。如果服务器私钥丢了,那所有的安全都无从谈起,所以 JWT 就是假设这个私钥不会丢。当然按我的理解,开发者也可以为每个用户设置一个单独的 secret,这就必须要面临上面提到的复杂性问题了。

关于 JWT 和 SAML 的对比,有一张很有意思的图片

OAuth 授权

OAuth 的设计本意更倾向于授权而不是认证,所以这一小节的标题写的是授权,但是其实在授权的同时也已经完成了认证。

本文偏向于认证,OAuth 在这里不过多讨论,更多 OAuth 内容可以参考这篇:理解 OAuth 2.0

3 SSO 与 CAS

接下来我们探讨一个企业应一定绕不过的课题:单点登录。

举例来说,华为云下有若干云服务。包含项目管理、代码托管、代码检查、流水线、编译构建、部署、自动化测试等众多微服务的 DevCloud(软件开发云)正是其中之一,用户如果在使用任意一个服务没有登录的时候都可以去同一个地方进行登录认证,登录之后的一段时间内可以无需登录访问所有其他服务。

在单点登录领域,CAS(Central Authentication Service,中文名是中央认证服务)是一个被高频使用的解决方案。因此,这里介绍一下利用 CAS 实现 SSO。而 CAS 的具体实现又可以依赖很多种协议,比如 OpenID、OAuth、SAML 等,这里重点介绍一下 CAS 协议。

CAS 协议中的几个重要概念

简单介绍一下 CAS 协议中的几个重要概念,一开始看概念可能很模糊,可以先过一遍,再结合下面的流程图来理解。

  • CAS Server:用于认证的中心服务器
  • CAS Clients:保护 CAS 应用,一旦有未认证的用户访问,重定向至 CAS Server 进行认证
  • TGT & TGC:用户认证之后,CAS Server 回生成一个包含用户信息的 TGT (Ticket Granting Ticket) 并向浏览器写一个 cookie(TGC,Ticket Granting Cookie),有啥用后面流程会讲到
  • ST:在 url 上作为参数传输的 ticket,受保护应用可以凭借这个 ticket 去 CAS Server 确认用户的认证是否合法

CAS 协议核心流程

介绍完概念,结合官方给出的流程图(先耐心地把图看一遍再看后面的流程解析效果更佳),对每一步进行详细的拆解,并点出几个可能会让人感到疑惑的问题。

① 用户通过浏览器访问受保护应用(以下简称 app_1)首页

② app_1 侧的 CAS Client 通过检测 session 的方式察觉到到用户未进行过认证,将用户重定向(第一次重定向)到 CAS Server,url 上携带的参数 service 包含了 app_1 的访问地址

③ CAS Server 检测到用户浏览器没有 TGC,提供表单让用户登录,用户登录成功后,CAS Server 生成包含用户信息的 TGT,并写 TGC 到用户浏览器

  • TGC 跟 TGT 相关联,是用户浏览器直接向 CAS Serv er 获取 ST 的票据,如果 TGC 有效,用户就不需要完成表单信息填写的步骤直接实现登录
  • TGC 的过期策略是这样设置的,如果用户一直没有页面操作和后台接口请求,那么默认 2 小时过期,如果一直有操作,默认 8 小时过期,开发者可以在 cas.properties 中对这两个过期时间进行修改,一般的应用中不会配置这么长的过期时间
# most-recently-used expiration policy
cas.ticket.tgt.timeout.maxTimeToLiveInSeconds=7200
# hard timeout policy
cas.ticket.tgt.timeout.hard.maxTimeToLiveInSeconds=28000

④ CAS Server 把浏览器重定向(第二次重定向)回 app_1 首页,此时重定向的 url 携带了 ST

⑤ app_1 再次接收到用户浏览器的访问,把上一步 url 参数中的 ST 拿出来,凭着 ST 去 CAS Server 确认当前用户是否已经完成认证,CAS Server 给出肯定回复以后,app_1 拿掉 url 上的 ST 重定向(第三次重定向)浏览器至 app_1 首页

  • app_1(CAS Client)凭借 ST 去向 CAS Server 确认当前用户认证状态的同时获取了包含用户信息在内的额外信息
  • 把这些额外信息写到 session 里并把 sessionId 返回给前端,那么前端下一次访问的时候直接判断 session 是否有效就可以了

⑥ 用户端浏览器去访问同一认证体系下的 app_2 首页

⑦ 同第②步,app_2 侧的 CAS Client 通过检测 session 的方式察觉到到用户未进行过认证,将用户重定向到 CAS Server,url 上携带的参数 service 包含了 app_2 的访问地址

⑧ CAS Server 检测到用户浏览器的 TGC,找到对应的 TGT,经验证是合法的,此处呼应了第③步的 TGC

⑨ 同第④步,CAS Server 把浏览器重定向回 app_2 首页,此时重定向的 url 携带了 ST

⑩ 同第⑤步,app_2 再次接收到用户浏览器的访问,把上一步 url 参数中的 ST 拿出来,凭着 ST 去 CAS Server 确认当前用户是否已经完成认证,CAS Server 给出肯定回复以后,app_2 拿掉 url 上的 ST 重定向浏览器至 app_2 首页

关于 CAS 流程中的几个问题

(1)如何避免 sessionId 冲突?

业务服务器(我们不妨把它看成跟前后文中提到的 CAS Client 是一个东西)通过在服务端写 session 并且把 sessionId 传回给前端保存的方式,保证用户登录的一段时间内不需要再次登录。那么如何保证使用同一单点认证的各个子服务(下文以服务 a 和服务 b 来举例描述)的的 sessionId 不冲突?当然这个问题的前提是服务 a 和服务 b 没有使用共享 session 的情况。

如果服务 a 和服务 b 使用了共享 session,那么他们的 sessionId 肯定是一致的,即两者的 CAS Client 在上述流程②中检测的 session 是一致的。此时如果用户已经在服务 a 登录,那么可以直接访问服务 b,因为在第②步的时候登录状态就已经验证通过。

如果服务 a 和服务 b 没有使用共享 session,那么用户在服务 a 登录之后,再去访问服务 b,要走上面流程中的第⑧步才可以确认用户是登录过的,此时,用户仍然不需要登录就可以访问,但是验证流程相比于共享 session 要长很多,如果你去观察 network 中的所有请求,也会发现在这个过程中多了几个 302。此处要讨论的问题恰恰是在这种情况下 a 和 b 如何避免 sessionId 冲突。

一旦发生冲突,就会导致用户在 a 和 b 之间切换的时候,双方的 CAS Client 需要不断地去 CAS Server 确认并刷新 session。这一段的描述如果不太好理解,可以往上翻一翻再看一遍上面流程图中的【First Access To Second Application】部分。其实要避免冲突也很简单,即 a 和 b 各自在自己写入前端 cookie 的 key 上加入服务名作为前缀 ,比如分别写成 a_sessionId 和 b_sessionId。

(2)假设 a 与 b 使用同样的单点登录认证 Server,有没有可能出现 a 应用登录过期,b 应用没有过期的情况?

接着上面的问题进行讨论,a 和 b 在不使用共享 session 的情况下,有没有可能出现这样一个情况:a 应用的认证状态过期了(session 和 TGC 都无效)而 b 应用仍没有过期(session 或 TGC 至少存在一个有效)?

答案是:不会。在业务实现中,CAS Client 会定期和 CAS Server 进行通信,如果用户一直在操作,那么 CAS Server 就会相应延长 TGC 的过期时间,最终对于 a 和 b 来说,TGC 的过期时间一定是相同的 。所以哪怕两边的 session 设置过期时长不一致,认证状态最多走到 CAS Server 处通过 TGC 的检测就能完成,而不会出现 a 需要登录,b 不需要登录的情况。

总结

本文首先探讨了认证与授权的区别,并列举了几种常见的认证与授权方式。然后重点介绍了一下使用 CAS 协议实现单点登录的流程与问题。最后,补充一点。华为云 DevCldoud 的 CAS Client 正是参考标准的 CAS 协议实现,感兴趣的同学可以在这里注册一个账号,然后打开 F12 使用账号登录观察所有的网络请求并分析一下 CAS 业务实现的完整流程。

加入我们

我们是 DevUI 团队,欢迎来这里和我们一起打造优雅高效的人机设计 / 研发体系。招聘邮箱:muyang2@huawei.com。


文 /DevUI 少东

参考文章

  • 傻傻分不清之 Cookie、Session、Token、JWT:https://juejin.im/post/5e055d…
  • SAML:https://www.cnblogs.com/maxig…
  • 理解 OAuth 2.0:https://www.ruanyifeng.com/bl…
  • JSON Web Token 入门教程:http://www.ruanyifeng.com/blo…
  • Introduction to JSON Web Tokens:https://jwt.io/introduction/
  • SAML2.0 入门指南:https://www.jianshu.com/p/636…
  • CAS protocol:https://apereo.github.io/cas/…
  • CAS Properties:https://apereo.github.io/cas/…
  • Ticket Expiration Policies:https://apereo.github.io/cas/…

往期文章推荐

《好用到飞起!VSCode 插件 DevUIHelper 设计开发全攻略(一)》

《Web 界面深色模式和主题化开发》

《手把手教你搭建一个灰度发布环境》

退出移动版