前后端鉴权二三事

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 少东