要想知道网站的登录方式为什么使用jwt还是得知道网站的发展历程来看。
为了方便叙事,年代记已经混淆,忽略即可,只是为了理清概念。
1.0 时代
最初的时候,网站都是纯粹的静态网站,基本上一台机器就能满足需求,每一次请求都会向后端请求新页面,此时用户的登录信息都是保存在cookie和session中的。
cookie是保存在客户端的,用户是可以篡改的,所以是不安全的。session是保存在服务器端的,用户是无法篡改的,所以是安全的。
可以看到这里面定义了一个WBPSESS
的cookie,盲猜这个是weibo security session
的缩写。
cookie是保存在客户端的,用户是可以篡改的。虽然session是保存在服务器端的,但是session的id是保存在cookie中的,我们可以替换成别人的session
id,这样就可以伪装成别人登录了。
session只是能保证存储信息是安全合法的,但是不保证传输过程是安全的。
cookie和session的传输都是浏览器自带的行为,不需要特殊处理,只要有cookie就会自动带上,所以这两种方式都是无感知的。
2.0 时代
后来,随着网站的发展,一台机器一个进程已经扛不住了,因此出现了一台机器多个进程,这样的话,session信息如果存储在内存中就无法共享了,用户的登录信息就无法共享了。
因此出现了分布式session,用户的登录信息都是保存在数据库中的,这样的话,用户的登录信息是安全的,因为数据库是保存在服务器端的,用户是无法篡改的。
但是这样的话,每次请求都需要向数据库请求用户的登录信息,这样的话,性能就会下降,时间耗费在io上。换成redis的话,情况是一样的。
同样的时期,ajax出现了。ajax是一种异步请求,可以不刷新页面就请求数据,web的访问方式就发生了变化,不再是每次请求都向后端请求新页面,而是向后端请求数据,然后前端自己渲染页面。
前端自己渲染页面的话,灵活性就会提高,用户体验就会提高,但是问题随之而来,比如CSRF(跨站请求伪造 cross-site request forgery)
攻击和XSS(跨站脚本攻击 cross-site scripting)攻击。
CSRF 攻击
CSRF攻击是一种利用用户身份的攻击方式,攻击者可以伪造用户的请求,这样的话,用户的登录信息就会被盗取。
比如:
- user登录了工商银行,工商银行给user分配了一个session id,保存在cookie中,然后user看了一下自己的账户余额,工资已经发下来了。
- user访问了不合法的网站,这个网站里面有一个img标签,src是工商银行的转账接口,这个接口是一个get请求,参数是转账金额和转账账户。
- user想放大图片,不断点击图片,这样的话,就会不断的向工商银行发送转账请求,因为已经登录了,所以cookie是会自动带上的,这样的话,就会不断的转账。
- 第二天,user再看自己的账户余额,发现自己的钱不见了。
整个过程十分隐秘,银行没有预警的情况下,user完全无法察觉。
XSS 攻击
XSS攻击是一种利用用户身份的攻击方式,攻击者可以在网页中插入恶意脚本,这样的话,用户的登录信息就会被盗取。
比如:
- user看到勇哥直播,想给勇哥打赏,于是点击了礼物。
- 但是评论区有人发了一个恶意评论,这个评论是一段js代码,这段代码直接修改了礼物充值的接口,把user的钱转到了攻击者的账户。
- user充值了1000元,但是发现自己的账户余额没有增加,而攻击者的账户余额增加了1000元。
为了应对这些情况,人们有了各种各样的解决方案,这不是今天的重点。
3.0 时代
为了应对各种问题,浏览器设置了同源策略,这样的话,不同域名的网站就无法访问对方的cookie了,这样的话,CSRF攻击就无法进行了。
什么是同源策略呢?
一个域名地址由协议、域名、端口、请求路径、查询参数组成。
如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。
浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。两个不同的源之间若想要相互访问资源或者操作DOM,那么会有⼀套基础的安全策略的制约,我们把这称为同源策略。
它的存在可以保护用户隐私信息,防止身份伪造等(读取Cookie)。
同源策略主要表现在 DOM、web数据 和 网络 这三个层面。
- DOM 层面:页面嵌页面(iframe)无法进行相互操作。
- web数据层面:限制cookie、localStorage和indexDB的读取,不同源的网站无法读取对方的cookie、localStorage和indexDB。
- 网络层面:限制跨域请求。
跨域过程
注意跨域过程:
- 浏览器向服务器发送请求。
- 服务器返回响应。
- 浏览器解析响应。
- 发现不是同源,拦截请求。
所以可以知道,跨域不是请求发不出去,而是请求发出去了,服务器正常接收请求并返回,但是浏览器拦截了响应。
跨域行为
同源策略是浏览器的行为,不是HTTP的规范。
但是有三个标签是允许跨域加载资源:
<img src=''>
<link href=''>
<script src=''>
因为以上内容允许跨域加载资源,所以一般优化的时候,会把静态资源放在cdn上,这样的话,用户访问网站的时候,就会向cdn请求资源,这样的话,用户的访问速度就会提高。
解决方法
什么时候会遇到跨域问题呢?比如”今日头条”。头条不可能所有的新闻都是自己的,所以会有很多新闻是从其他网站转载过来的,这样的话,就会遇到跨域问题,因为不同的新闻来源的是不同源的。
怎么解决跨域问题呢?国内一般就三种。
- JSONP
- 还记得有三种标签是允许跨域加载资源的吗?script标签就是其中之一,所以可以通过script标签加载资源,这样的话,就可以跨域了。
- JSONP就是利用script标签动态加载资源,然后在资源的回调函数中处理数据。
- 但是缺点也很明显,首先JSONP只支持GET请求,不支持POST请求。其次JSONP需要前后端配合,后端需要返回一个回调函数,前端需要处理这个回调函数,类似gRPC的方式了
- CORS
- CORS(cross-origin resource sharing)是一种跨域资源共享的方式,服务器设置。
- 浏览器只要获取到Access-Control-Allow-Origin的响应头,判断合法,就会放行请求。实现了跨域。
- 这种方式的优点是只需要后端配合,前端不需要做任何处理。
- 面试题:Access-Control-Allow-Origin是谁设置的?怎么设置的?
- 代理
- 在前后端之间添加一个转发的代理服务器,这样的话,就可以实现跨域了。
- 这种方式的优点是完全不需要前后端有任何配合,只要都面向这个额外的服务器开发即可。
- 面试题:为什么要用代理解决跨域问题?代理上不会跨域吗?
jwt
为啥要说到同源策略呢?还记得同源策略会限制什么吗?会限制cookie、localStorage和indexDB的读取。会导致什么问题呢?会导致session无法通过cookie传递,因为不同域名的网站无法访问对方的cookie了。
那怎么办?这时候jwt就出现了。(通过配置也能实现cookie传递,这里列举一般情况)
jwt(json web token)很早就有,但是在这个时候才开始大面积流行。jwt是 json web token 的缩写,是一种跨域传递非敏感信息的方式,它不存储在服务器端,而是存储在客户端。
一个典型的jwt长这样:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao |
你可能会有疑问,不是json吗?怎么是一串字符呢?先不忙,先看看jwt的组成,它是由三部分组成的,分别是header(头部)、payload(载荷)和signature(签名)。
header和payload都是json格式数据然后使用base64编码得到,因此想要获得真正的数据,只需要解码即可。
比如这里的header是:
1 | { |
他代表了这个jwt的加密方式和类型。
这里的payload是:
1 | { |
他代表了这个jwt的载荷,也就是用户的信息。
理论上payload是可以存储任何信息的,但是官方其实也给出了建议,如果你使用开源库的话,就会发现开源库对这些字段都有一定的校验。
- iss (Issuer):表示签发该 JWT 的发行者。
- sub (Subject):表示该 JWT 所面向的用户。
- aud (Audience):表示该 JWT 的预期接收者。
- exp (Expiration Time):表示该 JWT 的过期时间,UNIX 时间戳格式。
- nbf (Not Before):表示在该时间之前 JWT 不会被接受和处理,UNIX 时间戳格式。
- iat (Issued At):表示该 JWT 的签发时间,UNIX 时间戳格式。
- jti (JWT ID):表示该 JWT 的唯一标识符。
- scope:表示用户的权限。字符串数组。
- data:自定义字段。存放用户自定义的信息。
从这里可以看到,jwt信息十分容易获取,因此jwt是不适合存储敏感信息的。
签名是由header、payload和盐加密得到的,具体过程略,这样的话,就可以保证jwt的完整性,如果有人篡改了jwt,那么签名就会不匹配。
和jwt一起经常出现的两个名词还有jws(json web signature)和jwe(json web encryption)。这两个都是开放标准,用于在jwt上进行签名和加密。
jws就是只对内容进行签名,内容本身不加密。与我们常见的jwt类似。jwe是对内容进行加密,然后再对加密后的内容进行签名。这样的话,就可以保证内容的机密性和完整性。当然成本也会提高。
至此,jwt开始崭露头角,但是查看它的过程就知道,从session到jwt,其实是将io的时间转移到了cpu上,因为每次请求都需要解码jwt,大量请求的话,cpu的负担就会很重。
面试题:
- session和jwt本质上就不同,为什么总是拿来比较?
- 为什么有些系统会选择jwt,有些系统会选择session?
题外话时间
题外话01:jwt的签名安全吗?
不断会有人说jwt替换了session是因为jwt更加安全,用户无法修改。然而,事实真的是这样吗?
我们也知道jwt的第三段是签名,那么我们不要这个签名行不行呢?答案是可以的,因为jwt的加密方法在header中,所以我们可以直接修改header,定义
alg 为 none,这样的话,就可以得到一个没有签名的jwt了。
如果组件是开源框架,不是自己写的,那么这个框架会通过header中的 alg 来判断是否需要签名,如果 alg 是 none,那么就不会进行签名。
此时,你修改的是哪个用户,你现在就是哪个用户了。
题外话02:jwt真的简化了取数据的流程了吗?
并没有,我们之前也说了,jwt只适合存储非敏感信息,而且数据量不能过大,否则每次传输也会占用网络带宽。
那么需要获取具体信息怎么办呢?
这时候就要根据jwt的载荷定义的用户标识,然后去数据库中查询对应用户表以及关联表等。本质上是查询延后了而已,并没有简化,而大多数视频只谈论前半部分步骤,貌似较少了io。
题外话03:token 是什么?
在手机app、小程序等场景中,我们没有办法直接使用cookie/session,于是一些人定义了token,这个token是一串字符,用来代表用户的身份。这个token是保存在客户端的,每次请求的时候,都会带上这个token。
有没有似曾相识的感觉?
没错,这个token就是另外一种形式的session,只不过session浏览器会自动传递,而token还得自己维护。
题外话04: API Key 是什么?
API Key 是一种用于访问 API 的密钥,它是一串字符,用来代表用户的身份。这个 API Key 是保存在客户端的,每次请求的时候,都会带上这个
API Key。他已经被绝大多数开放API所使用。
jwt、cookie、API Key的对比如下:
jwt | cookie | API Key | |
---|---|---|---|
应用场景 | 前后端、后端服务 | 前后端 | 后端服务 |
认证对象 | 主要是用户 | 用户 | 系统、应用 |
撤销 | 不方便 | 方便 | 方便 |
生成方式 | 动态生成 | 动态生成 | 预先分配 |
jwt 缺点如下:
- jwt无法实时退出所有client,因为jwt是保存在客户端的,用户是无法篡改的,所以只能等待jwt过期。
- 信息修改无法及时同步。比如jwt存储了用户的信息,而此时用户修改了信息,但是jwt是不会自动更新的,只能等待jwt过期。
- jwt泄露无法马上将token无效。
三种认证方式各有优劣,没有绝对的好坏之分,只有适合不适合。很多网站也会把jwt和cookie结合使用,jwt存储用户的信息,cookie存储jwt,简化开发,也防止乱存的jwt泄露。
4.0 时代
看起来网站已经和现在差不多了,还有什么发展呢?
其实也不算发展,只是起了一个名字,同一时代,jwt流行起来,还有个技术流行起来,就是分布式系统。
分布式系统和jwt一样,自古有之,但是认证问题还没有统一。此时一个用户可能会访问多个系统,每个系统都需要认证且认证系统不一样的话,不仅麻烦还不安全。
一种解决方案就是单点登录(SSO,single sign-on)。
SSO是一种认证机制,用户只需要登录一次,就可以访问多个系统,这样的话,用户的登录信息就是安全的,因为用户只需要登录一次。
CAS
目前比较流行的就是CAS(Central Authentication Service 中央认证服务器)。CAS是一种开源的单点登录协议,它的原理是这样的:
- 用户访问网站A,网站A发现用户没有登录,就会重定向到CAS服务器。
- 用户向CAS认证,然后CAS服务器会给用户一个ST(Service Ticket),一个TGT(Ticket Granting Ticket),并且重定向到网站A。
- 用户拿着ST去网站A,网站A获取到ST
- 网站A不知真假,拿着ST和自己的服务标识去CAS服务器验证,验证通过返回给网站A。
- 网站A得到验证通过的信息,就会给用户创建session或者jwt。
- 只要session或者jwt没有过期,用户就可以一直访问网站A。
以上是用户访问网站A的过程,用户访问网站B的过程是一样的。
- 用户继续访问网站B,网站B发现用户没有登录,就会重定向到CAS服务器。
- 但是此时用户已经有了TGT,所以不需要再次登录,CAS服务器会直接给用户一个ST,然后重定向到网站B。
- 用户拿着ST去网站B,网站B获取到ST……之后的步骤和网站A一样
CAS的过程对于用户是透明的,用户的感受是只需要登录一次,就可以访问多个系统。当然上面的过程是简化的,实际上还有很多细节。
OAuth2和OIDC介绍
OIDC(OpenID Connect)
是OpenID的升级版,OpenID是一种认证协议,OIDC是一种认证协议,它是基于OAuth2的,OAuth2是一种授权协议。OpenID的官网在这里:https://openid.net/
OAuth2是一种授权协议,现在一般都是使用OAuth2,OAuth1已经被淘汰了。OAuth2的文档在这里:https://oauth.net/2/
在继续往下讲之前,我们先来认清两个概念:
- 认证(authentication):是确认用户的身份,比如用户名和密码、指纹、人脸识别等。
- 授权(authorization):是确认用户的权限,比如用户有没有权限访问某个资源。
OAuth2是一种授权协议,它的目的是让用户授权第三方应用访问自己的资源,比如用户授权第三方应用访问自己的微博、微信等。
为什么说OAuth2是一种授权协议呢?因为第三方仅需要用户的授权即可,它不关心用户是谁,有没有认证。
实际情况中只实现了OAuth2的应用是比较少的,还往往会实现OIDC,口语中将两者统称为OAuth2,实际上是有些不准确的。
OIDC = OAuth2 + 认证机制,是一种OAuth2的升级版,经常用于联合登录或单点登录的场景。
为什么不迭代OAuth2,而是新定义了一个OIDC呢?正所谓一流企业做标准,二流企业做品牌,三流企业做产品,OpenID已经走上了从标准到品牌的道路,只能说利益驱动。
OIDC过程
这里我们通过腾讯会议通过微信登录的过程来了解一下OIDC的过程。
- 我(用户)打开腾讯会议(客户端 client),然后点击微信登录。
- 腾讯会议(客户端 client)向微信平台(认证服务器 authorization server)发起请求,传入了自己的 client_id、scope和callback_url。
- 微信窗口提示我(用户)登录,同时提醒我账号将访问我的哪些信息。我(用户)点击屏幕进行了登录。
- 微信平台(认证服务器 authorization server)验证我(用户)的身份,然后通过callback_url重定向到腾讯会议(客户端
client)并返回了code(授权码)。 - 腾讯会议(客户端 client)拿着code(授权码)向微信平台(认证服务器 authorization server)发起请求,传入了自己的
client_id、client_secret、code - 认证成功后返回了access_token(访问令牌)、refresh_token(刷新令牌)、openid(用户标识)和 unionid(用户统一标识)。
- 腾讯会议(客户端 client)会根据unionid去自己的数据库中查询用户信息,如果存在则返回登录状态。
- 如果不存在则会通过 openid 去微信平台(认证服务器 authorization server)获取用户信息,然后保存到自己的数据库中,然后返回登录状态。
OIDC和OAuth2的比较
角色
OAuth2中有四种角色:
- 客户端(client),就是腾讯会议。
- 资源拥有者(resource owner),就是我。
- 授权服务器(authorization server),就是微信平台。
- 资源服务器(resource server),就是腾讯会议的数据库。
OIDC聚焦在认证环节,所以没有资源服务器的角色,其他三种也有所变化:
- 客户端称作 Relaying Party,就是腾讯会议。
- 资源所有者称作 终端用户(End-User),就是我。
- 认证服务器称作 OpenID Provider,就是微信平台。
CAS 可以使 openid 的 provider,也就是 openid 的 provider 可以承担单点认证中的 CAS 的角色。
授权方式
OAuth2中有四种授权方式:
- 授权码模式(authorization code),就是上面的过程,优点是不需要暴露用户的密码,就可以获取用户的授权,目前使用最多。
- 简化模式(implicit)
- 密码模式(password)
- 客户端模式(client credentials)
服务接口
OAuth2中定义了两个服务接口:
- 授权服务接口(authorization endpoint),用于获取授权码。
- 令牌服务接口(token endpoint),用于获取访问令牌。
OIDC中新增了一个userinfo服务接口,用于获取用户信息。
令牌
scope定义了具体需要获取的权限,由服务实现方定义,没有统一的标准。OIDC中新增了openid的scope,令牌借口的返回响应中会包含openid属性,这个ID是一个jwt
OAuth2中定义了两种令牌:
- 访问令牌(access token),用于访问资源。
- 刷新令牌(refresh token),用于刷新访问令牌。
OIDC中新增了一种令牌 openid,当 scope 中指定 openid 时,就会获得 openid 令牌。