- Published on
前端登录知多少
- Authors

- Name
- McDaddy(戣蓦)
前端登录知多少
当我们日常访问网页或者网站应用时,一旦涉及到权限问题就需要我们进行登录验证,同时在登录成功后还能在很长一段时间保持这个登录的状态,而不需要二次登录。所以这是怎么做到的呢?
根因
一切的起源在于当我们把用户名密码提交给服务端后,看似让服务端“认识”了我们,但是当下一次发起客户端请求时,服务端又不知道这是谁了。 其中根本的原因就是HTTP是无状态的。即HTTP请求A是无法一个所谓的“登录状态”继承给HTTP请求B的
解决方案
所以这个解决方案的核心就是存储,如果把登录的信息存储在客户端,好让每次请求都带上它,让服务端认得自己
方案1
直观的看,最容易想到的方法就是,能不能每次请求都把账号名+密码带在请求体中,这样等于每次请求都可以验证自己的身份了。但这样的问题会非常多
- 需要由浏览器去托管我们的账号密码,有泄露的风险
- 每次请求都校验身份,效率很低
- 浏览器本身并没有这个功能(每次请求通过浏览器注入参数)
所以这是一个实际上并没有存在过的方案
方案2
通过上个方案,我们的思路就是想怎么在浏览器里存信息,同时能让请求带上它。这里就要引出cookie
cookie简单的说就是一系列存储在浏览器上的键值对,它有几个主要的信息
- Name和Value,也就是Key和Value
- Domain:代表是由哪个域名注入的cookie,如果域名和当前访问网站一致就是一方cookie,否则就是三方cookie。
- Expires: 过期时间,过了这个时间这个cookie就会自动被删除

cookie是通过服务端的返回,一般是类似login的接口的成功返回中,在Response Header中添加了一个叫做Set-Cookie的头,里面的内容就是一个键值对的字符串,这里一般就是用户相关的登录信息了

接下来就是cookie最强大的功能了,在每次XHR请求中,它都会自动把当前域的cookie带上,放在请求头中的Cookie字段里面(跨域情况除外),这样每次访问服务端就会自动带上由服务端上次Set给客户端的用户登录信息,所以就能做到登录状态保持了

但这里会有一个问题,即服务端在set-cookie中应该放什么内容让客户端存储呢? 假设我们延续了方案1的思路,在Cookie中放入账号名和密码,理论上是解决了登录保持的问题,但上面提到的风险隐患依然存在
方案3
Session + Cookie
基于方案2,服务器在每次收到登录请求之后,都会生成一个响应的session,也就是会话,然后存储在服务器的存储介质里,比如redis,同时通过Set-Cookie的方式把SessionID返回给客户端
这样,当下次客户端正常请求发来时,就会自动携带上包含SessionID的Cookie信息,服务器通过查询校验Session信息,从而验证了用户的信息,客户端由此也保持了登录态
这个方案在很长一段时间都是业界的标准实现方案,但这个方案有没有问题呢? 当然有,而且很多
CSRF
因为 cookie 会在请求时自动带上,那你在一个网站登录了,再访问别的网站,万一里面有个按钮会请求之前那个网站的,那 cookie 依然能带上。而这时候就不用再登录了。
一般这种利用 CSRF 漏洞的网站都会伪装的很好,让你很难看出破绽来,这种网站叫做钓鱼网站。
面对这种问题,最简单的解决办法是在后端验证请求的referer,就是说看看这个请求是从哪个地方发起的,只有是自己的域名才继续执行。但如果浏览器也被黑了,伪造referer怎么办?
为了解决这个问题,目前业界标准解决方法是CSRF token,也称为双重cookie校验法,就是服务端生成一个随机码放在cookie里返回,后面的每次请求,前端通过代码把这个随机码取出来,然后放到一个类似叫csrf-token这样的一个header中,同时也会带上这个随机码在cookie中,最后服务端收到后分别取出cookie中的随机码和header中的token做对比,如果相同就放行
这里的一个原理就是,js代码仅可以读取属于自己域名或者子域名和父域名的Cookie值,比如运行在a.taobao.com的js代码,只可以取到a.taobao.com以及xx.a.taobao.com和taobao.com的所有cookie,但是b.taobao.com以及其他任何域名的cookie它都是拿不到的。 所以在钓鱼网站上,它虽然可以转发你的cookie但是无法通过代码读取到,也就无法添加额外的header
所以这是 session + cookie 这种方案的一个缺点,但是是有解决方案的。
分布式session
现代服务端为了承载更高的流量基本都是走分布式架构,但之前的方案里session只存在一个单点机器上,下次服务端如果没有命中这台机器就等于没有Session,还是无法保持登录态
为了解决这个问题,一般有两个解决思路
- 复制session,即通过同步的手段让每个服务节点都有同样的session拷贝。实现方式如spring-session
- 把session存在一个单点Redis服务上,不管请求到达哪个节点,都去访问这个Redis服务,用来校验session,这样就不需要复制了
目前一般是用session + redis这个方案更多
跨域
上面我们在csrf提到过cookie是有区分域名的,默认情况确实是会自动随着请求携带,但是如果在跨域的情况下,比如在taobao.com的页面上访问tmall.com的接口,是不会主动带任何cookie的,此时前端必须手动设置withCredentials属性为true以强制带上cookie
同时,在服务端也必须设置对应的Response Header
Access-Control-Allow-Origin: "当前域名";
Access-Control-Allow-Credentials: true
这里的 allow origin 设置 * 都不行,必须指定具体的域名才能接收跨域 cookie。
虽然这三个问题都可以解决,但需要适配和注意的地方也很多。 所以有没有别的可行方案呢?
方案4
JWT token
之前的解决方案基本都是把状态存储在服务端,返回ID,让cookie携带id来让服务器校验。那我们能不能实现不需要服务端做任何存储,把登录状态仅仅维护在客户端呢?
JWT全称JSON Web Token,它是一个分为三部分的字符串
- 头部header:保存计算token使用的算法,目前主流使用HS256
- 信息体payload:主要保存一些用户信息
- 消息签名verify signature:用来保存签名,它是通过header中的算法,将payload中的信息 + header + salt(有且仅有服务端私有)生成出来的字符串

将以上三部分都做Base64编码,就得到了jwt token的字符串。
服务端把jwt token返回给客户端,客户端在下次请求时放在header中比如x-jwt-token这个字段里。
服务端拿到token,通过Base64解码,拿到header和payload,然后通过同样的算法,计算出一个签名,和最后那段传来的签名做对比

通过这个方案,可以完美解决session的三个问题
csrf:因为整个方案和Cookie无关,所以不存在csrf问题
分布式问题: 因为状态保存在客户端,服务端只需要统一管理salt即可,不论访问哪个节点都能正确解析token
跨域:同csrf,没有cookie跨域的限制,就没有这个问题
但这样是不是就一点问题都没有? 当然不是
安全性
因为JWT把用户信息直接放在header里,仅仅是Base64编码,不是加密,所以一旦被人劫持,就会暴露敏感信息,甚至直接通过你的jwt去做请求
所以现在使用JWT的场景必须搭配https
性能
jwt token的长度往往是比一个session ID长很多的,每次都带上会影响带宽
没法让jwt失效
相比session存在服务端,如果我想实现让人强制登出,只要删除这个session即可
但是jwt因为是存在客户端,虽然token信息里一般都带有exp时间戳,这个在生成的时刻就确定了,也就意味着不论发生什么,在token过期前,服务端都无法阻挡此用户的操作
当然如果非要做到踢人的话,那就需要配合redis来记录每个token的状态,每次请求进行校验,这个原理就和session基本没差了
所以一般都会把jwt的过期时间设置比较短,比如一小时,到期前让前端去刷token,以此来实现token续期
那是不是现在不管什么场景都用jwt呢?
肯定不是,jwt比较适合软件的用户登录,比如大多数企业级应用,它的特点是数据敏感,权限校验比较严格,一段时间不操作就可以把人登出。 相反那些普通电商或社交场景,往往一次登录后N天都不需要重新登录,即使中间不做任何操作,这种情况jwt就不太合适了,还是要采用session + cookie的模式
Next
OAuth2
扫码登录