互联网年代,一个网站的用户数,就是这个网站的命脉,那么,这些用户的账户安全问题很成问题,于是行业大佬们,开始忧国忧民,研究出很多解决当下痛点的解决方案。从最开始的前后端不分离,研究出来的session-cookie,到后来基于前端存储的Token 验证 ,后来网站越来越多多了,为了不总是注册账号推出来的OAuth权限,以及一个公司项目太多了,为了防止重复登录开启的单点登录。
到这里你就会发现,我们每一步的鉴权方案,其实就是一个前端的行业成长史,每一种方案,都在特定的历史环境下,解决了一些痛点,并且在行业日新月异的情况下,完成了历史使命,而退出舞台。
在我看来透彻的了解目前的前端鉴权方案,除了有利于我们面试找工作。同样的他能帮助你了解前端这个工种,是怎样从一个简单的切图仔一步步成长为工程师的。是有多少前辈在刀耕火种的年代,不畏艰难积累知识,了解过去。开枝散叶,无私奉献,才有了我们今天的高薪。繁荣
于是,你开始满怀敬畏,空杯心态,努力学习,乐于助人,积极进取 .........吧啦吧啦吧啦.......
以上内容纯属扯淡,就到这吧!下面开始正题,本文为某最近整理前端鉴权机制的大致体系留下来的一些笔记,如有帮助请点赞,如有错误请指出,如已了然,请全当乐呵!
本文大体从时间线,以及解决何种问题,和大致底层原理来阐述前端鉴权的几种常用方案。请各位看官批评!
首先登场的是session-cookie机制,这是出现时间最早,也是使用最多的鉴权方式(因为现在很多老系统还在维护)。
在正式开讲之前我们先了解一什么叫cookie,什么叫session, 为啥要有这两种产物
我们知道互联网的兴起离不开网络,网络就离不开协议,而在我们前端中打交道的就是http协议,然而,他好死不死的是个无状态协议,就是在传输过程中不记录前后端交互的一些状态,在互联网迅猛发展的过程中,就有了用户的概念,一个网站也不是纯展示为主,于是服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session,上述内容可能还有伙伴不太明白,接下来举个例子
典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。
到这里你就会发现其实Session就是一个信息,并且有一个唯一标识,来识别身份
刚才说到,session是一个信息,这样范围就很广了,他像是一个抽象的概念,而cookie 则是一个真实存在的东西,他是客户端或者浏览器的一种技术,说白了就是一种存储内容的技术,并且会在客户端发起http请求的时候携在http的header中。
ok 说完这两者之后我们就应该明白了他们两者的区别和联系,接下来就来验证一一下session-cookie 的鉴权机制到底是怎么样的
如上图所示,所谓session-cookie模式就是将服务器保存信息,生成一个唯一标识,然后下发给客户端,保存在cookie中,这样每次客户端就能使用这个唯一标识去判断用户的一些状态以及登录信息,这就是session-cookie的原理。
其实本质很质朴,但是由于互联网的发展,只是简单的生成一个唯一标识很容易被人破解,于是,又开始了折腾之路。
由于最开始的简单生成一个cookie,很容易被人模仿,和伪造,和篡改,这样用户信息就会泄露,于是,引入Hash算法,来验证当前用户身份,防止伪造。
哈希(hash)算法又称为散列算法,通过hash算法,可以将任意长度的信息转换成一个固定长度的二进制数据,我们经常会使用十六进制值来表示转换后的信息。
基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。活动开发中经常使用的MD5和SHA都是历史悠久的Hash算法。
ok 说了这么多,hash算法有什么特点呢?
在密码学中,hash算法的作用主要是用于消息摘要和签名,换句话说,它主要用于对整个消息的完整性进行校验。
举个例子,我们登陆掘金的时候都需要输入密码,那么掘金如果明文保存这个密码,那么黑客就很容易窃取大家的密码来登陆,特别不安全。所以,他就使用hash算法生成一个密码的签名,数据库保存这个签名值。由于hash算法是不可逆的,那么黑客即便得到这个签名,也丝毫没有用处;因为他反推不出来这个密码。而如果你在网站登陆界面上输入你的密码,那么提交请求的时候重新计算一下这个hash值,与网站中储存的原hash值进行比对,如果相同,证明你拥有这个账户的密码,那么就会允许你登陆。银行也是如此,银行是万万不敢保存用户密码的原文的,只会保存密码的hash值而而已。
而在我们的sessionId 中加入hash算法来生成签名其实和密码的这个例子类似,是为了防止黑客拿到sessionid 但是由于hash算法生成了一个签名,那么如果在访问接口的时候不匹配,那么就可以判断当前用户是伪造的。
在了解完了cookie-session 原理之后,我们发现cookie-session有着很多不足,这时大佬们有开始研究了。这就是后来的token 验证,我们先来看看cookie-session的不足
于是大名鼎鼎的tokne模式横空出世,这也是当下所有企业级项目普遍的鉴权方式,那么他有什么特点呢?
那么他是怎么工作的呢?
到目前为止我们先来对比一下Token与cookie-session的区别
明白了工作流程,咱来来一起研究下原理,之前我们说过token鉴权是有标准的比如jwt,那么我们就来解析jwt原理
一个 JWT token 是一个字符串,它由三部分组成,令牌头、载荷(payload)与签名(Signature)
那么他长生么样呢?
如上图所示,就长这个样子,三个部分直接用.分隔,接下来我们一个个解析
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
{
"alg": "HS256",
"typ": "JWT"
}
然后用 Base64Url 编码得到头部,即 ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9。
JWT签名算法中,一般有两个选择,一个采用HS256,另外一个就是采用RS256。 签名实际上是一个加密的过程,生成一段标识(也是JWT的一部分)作为接收方验证信息是否被篡改的依据。
RS256 (采用SHA-256 的 RSA 签名) 是一种非对称算法, 它使用公共/私钥对: 标识提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护, 因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据URL)。 另一方面, HS256 (带有 SHA-256 的 HMAC 是一种对称算法, 双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。
在开发应用的时候启用JWT,使用RS256更加安全,你可以控制谁能使用什么类型的密钥。另外,如果你无法控制客户端,无法做到密钥的完全保密,RS256会是个更佳的选择,JWT的使用方只需要知道公钥。
由于公钥通常可以从元数据URL节点获得,因此可以对客户端进行进行编程以自动检索公钥。如果采用这种方式,从服务器上直接下载公钥信息,可以有效的减少配置信息。
载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换。
载荷的属性也分三类:
我们先来看预定义,这里面的前 7 个字段都是由官方所定义的,也就是预定义(Registered claims)的,并不都是必需的。
{
"sub": "1", //主题
"iss": "http://localhost:8000/auth/login",//该JWT的签发者
"iat": 1451888119,//在什么时候签发的
"exp": 1454516119,// 什么时候过期,这里是一个Unix时间戳
"nbf": 1451888119,//生效时间,在此之前是无效的
"jti": "37c107e4609ddbcc9c096ea5ee76c667",//编号
"aud": "dev"//受众
公有的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有声明
{
"sub":"1234567890",// 预定义
"name":"hore_brother",
"admin":true
}
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的声明。比如前面那个结构举例中的admin和name都属于自定的私有声明。这些声明跟JWT标准规定的声明区别在于:JWT规定的声明,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的声明进行验证(还不知道是否能够验证);而私有声明不会验证,除非明确告诉接收方要对这些声明进行验证以及规则才行。
将上面的 json 进行 Base64Url 编码得到载荷,,即 CnsKICAic3ViIjogIjEiLAogICJpc3MiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDAwL2F1dGgvbG9naW4iLAogICJpYXQiOiAxNDUxODg4MTE5LAogICJleHAiOiAxNDU0NTE2MTE5LAogICJuYmYiOiAxNDUxODg4MTE5LAogICJqdGkiOiAiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjciLAogICJhdWQiOiAiZGV2Igp9。
注意:base64是一种编码,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
jwt的签名信息由三部分组成:header (base64后的),payload (base64后的),secret(秘钥)
拿到这三部分之后,使用加密算法加密之后编码生成最后的签名部分。如果以 HMACSHA256 加密,
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
你是不是觉得很麻烦,其实社区已经有封装的库了。
// jsonwebtoken.js
const jsonwebtoken = require('jsonwebtoken')
const secret = '12345678'
const opt = {
secret: 'jwt_secret',
key: 'user' }
const user = {
username: 'abc',
password: '111111'
}
const token = jsonwebtoken.sign({
data: user,
// 设置 token 过期时间
exp: Math.floor(Date.now() / 1000) + (60 * 60),
}, secret)
console.log('⽣生成token:' + token)
// ⽣生成 token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWJjIiw icGFzc3dvcmQiOiIxMTExMTEifSwiZXhwIjoxNTQ2OTQyMzk1LCJpYXQiOjE1NDY5Mzg3OTV9.VPBC QgLB7XPBq3RdHK9WQMkPp3dw65JzEKm_LZZjP9Y
使用上述库就能通过简单的api实现token的生成 并且由于签名部分的加密秘钥在服务端,即实现了在客户端携带信息,并且防止篡改。有的人又会问了,他为啥能防篡改呢?
HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。
OAuth是在现在的互联网中的使用非常多,举个例子,我们登陆一个网站,你会发现网站上有qq登陆,微信登录,github 登录,谷歌登录等,其实就是OAuth开放授权。
三⽅登⼊主要基于OAuth 2.0。OAuth协议为⽤用户资源的授权提供了⼀个安全的、开放⽽⼜ 简易的标准。与以往的授权⽅式不同之处是OAuth的授权不会使第三方触及到⽤户的帐号信息 (如⽤用户名与密码),即第三⽅方⽆无需使⽤户的⽤户名与密码就可以申请获得该⽤户资源的授权, 因此OAuth是安全的。
了解之后,我们就来简述一下他的原理。由于在我们日常开发中,每个平台的对接方式都不一样,我们就以github为例。大致去还原一下想要实现OAuth授权登录,需要的操作流程,在这里只是简述,如想深入了解,请去github开发者中心。
假设我有一个网站,并且有用户体系,每个用户需要有一个账号,此时,有个新来的用户,想要在当前的帖子上留言,但是他有不想注册账号并且还想留言 此时怎么办呢? 这是你会发现,很多大型的网站,他已经注册了。比如github,并且github提供了一套鉴权机制让你能这个当前网站能访问到github的用户信息,如此一来,你就能访问就能达到双赢局面,用户既省去了注册的繁琐步骤,你又留住了一个用户,在互联网慢慢的发展中这套鉴权机制就叫做OAuth
接下来我们来开始一步步揭露他的原理步骤,其实,所谓的OAuth鉴权他的本质就是两个协商的过程,从而确定网站以及用户的一一对应关系,并且获取用户的意向(同意还是不同意)
再根据协商结果去做进一步操作,从而确定访问权限。
我们首先需要知道的是。即使用户刚登录过 github,我的网站也不能随便拿到用户消息,这显然是侵犯用户隐私的,是不安全。所以,我们必须去进行第一步的协商
像github这种世界级别的网站,他对授权有着严格的权限分类,比如读取仓库信息的权限、写入仓库的权限、读取用户信息的权限、修改用户信息的权限等等。如果我想获取用户的信息,Github 会要求我,先在它的平台上注册一个应用,在申请的时候标明需要获取用户信息的哪些权限,用多少就申请多少,并且在申请的时候填写你的网站域名,Github 只允许在这个域名中获取用户信息。
此时,如果我如果填了申请,并且审批通过,那么我算是入了github的白名单了,github会给我发两个门票,一张门票叫做 Client Id,另一张门票叫做 Client Secret。
到这里,第一步就算踏出去了
用户进入我的网站,点击 github 登录按钮的时候,我的网站会把上面拿到的 Client Id 交给用户,让他进入到 Github 的授权页面,Github 看到了用户手中的门票,就知道这是我的网站让他过来的,因为一个网站对应一个Client Id,于是它就把我的网站想要获取的权限摆出来,并询问用户是否允许我获取这些权限。此时,就是最关键的一步,获取用户授权,防止侵犯隐私。
在代码上表示其实就是,我去请求一个登录地址,然后带上一个令牌和成功之后的回调地址
// 用户登录 github,协商
GET https://github.com/login/oauth/authorize
// 协商凭证
params = {
client_id: "xxxx",
redirect_uri: "http://my-website.com"
}
如果用户觉得我的网站要的权限太多,或者压根就不想我知道他这些信息,选择了拒绝的话,整个 OAuth 2.0 的认证就结束了,认证也以失败告终。如果用户觉得 OK,在授权页面点击了确认授权后,页面会跳转到我预先设定的 redirect_uri
并附带一个盖了章的门票 code。
// 协商成功后带着盖了章的 code
Location: http://my-website.com?code=xxx
这个时候,用户和 Github 之间的协商就已经完成,Github 也会在自己的系统中记录这次协商,表示该用户已经允许在我的网站访问上直接操作和使用他的部分资源。
执行上面两步之后,鉴权的主体已经完成,剩下的我们只需要拿着code码去获取github
第二步中,我们已经拿到了盖过章的门票 code,但这个 code 只能表明,用户允许我的网站从 github 上获取该用户的数据,如果我直接拿这个 code 去 github 访问数据一定会被拒绝,因为任何人都可以持有 code,github 并不知道 code 持有方就是我本人。
还记得之前申请应用的时候 github 给我的两张门票么,Client Id 在上一步中已经用过了,接下来轮到另一张门票 Client Secret。
如此一来,双证齐全,就能知道当前的这个授权用户是从我这个网站出来的。于是,就会给你发一个最终通行证,也就是access_token,模拟代码如下
// 网站和 github 之间的协商
POST https://github.com/login/oauth/access_token
// 协商凭证包括 github 给用户盖的章和 github 发给我的门票
params = {
code: "xxx",
client_id: "xxx",
client_secret: "xxx",
redirect_uri: "http://my-website.com"
}
// 拿到最后的通行证
response = {
access_token: "e72e16c7e42f292c6912e7710c838347ae178b4a"
scope: "user,gist"
token_type: "bearer",
refresh_token: "xxxx"
}
上一步 github 已经把最后的通行证 access_token 给我了,通过 github 提供的 API 加通行证就能够访问用户的信息了,能获取用户的哪些权限在 response 中也给了明确的说明,scope 为 user 和 gist,也就是只能获取 user 组和 gist 组两个小组的权限,user 组中就包含了用户的名字和邮箱等信息了。
// 访问用户数据
GET //api.github.com/user?access_token=e72e16c7e42f292c6912e7710c838347ae178b4a
// 告诉我用户的名字和邮箱
response = {
username: "好学习吧",
email: "8888888.abcd@gmail.com"
}
到此为止简单的 OAuth登录认证就到此结束
所谓单点登录全称叫做:Single Sign On(简称SSO),那究竟什么是单点登录呢?
在最初的互联网系我们就是单系统,所有的功能都在同一个系统上。
后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统,
而此时,问题来了,我多个系统之间是不通信的,那么如何去获取用户的登录状态呢?比如:淘宝和天猫是两个独立的系统,我们想要保证两个系统的登录状态一致,用户信息同步。就会用到单点登录。
单点登录简单的说就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
知道了什么是单点登录以后,他的原理其实很简单,而我们的实现方式其实看起来也很简单,就是无论使用什么鉴权模式,只需要让多个系统之间的认证信息互通即可。但是我们却需要解决以下问题。
接下来我么一个个解决
由于多系统之间Session不共享,那么此时就必须有一个公共的地方去存储当前的这个session,如此一来,答案就可以呼之欲出了比如,开一个公共的Redis,或者登录功能单独抽取出来,做成一个子系统等
上面我们解决了Session不能共享的问题,但其实还有另一个问题。Cookie是不能跨域的,其实也就是浏览器同源策略的限制。那么解决方案,其实也相对清晰。比如:绕开同源策略。两个网站使用同一个一级域名、或者强制推送cookie 将Token保存在SessionStroage中 在请求的时候携带上等
第三个问题,其实是当前的的一个最佳实践,有着一个成熟的解决方案,叫做CAS (Central Authentication Service)
如下图就是他的总体原理图
接下来我么 来一步步解析
首先模拟了三个服务,分别是CAS、系统A、系统B,它们分别部署在cas.com,systemA.com和systemB.com;CAS这个服务用来管理SSO的会话;系统A和系统B代表着实际的业务系统。我从五个场景分别来说明这个SSO方案的实现细节。下面先来看第一个。
在当前第一个场景中,他的第一个场景在于
从这一步可以看出,即使登录之后,也要每次跟CAS校验jwt的有效性以及会话的有效性,其实jwt的有效性也可以放在业务系统里面处理的,但是会话的有效性就必须到CAS那边才能完成了。当CAS拿到jwt里面的session id之后,就能到session 缓存服务器里面去验证该session id对应的session对象是否存在,不存在,就说明会话已经销毁了(退出)。
这个过程的关键在于第一次重定向的时候,它会把sid这个cookie带回给CAS服务器,所以CAS服务器能够判断出会话是否已经建立,如果已经建立就跳过登录页的逻辑。
最重要的是要清除sid的cookie,jwt的cookie可能业务系统都有创建,所以不可能在退出的时候还挨个去清除那些系统的cookie,只要sid一清除,那么即使那些jwt的cookie在下次访问的时候还会被传递到业务系统的服务端,由于jwt里面的sid已经无效,所以最后还是会被重定向到CAS登录页进行处理。
到此为止,sso 单点登录基本结束了,我们只做了解即可,因为大部分业务中基本用不上,我们到目前为止做的单点登录就是携带token跳转,虽然不安全,但是能快速应用。在面对快速业务开发的我们。是个最优选择
首先感谢巨人
OAuth 授权的工作原理是怎样的?足够安全吗? 看图理解JWT如何用于单点登录
断断续续很多天,终于将基本的用户鉴权机制的原理整理完成,在我整理完并且自己形成自己的知识体系之后分享出来,希望对大家有帮助!