WEB 系统登录相关知识梳理
前言
一直以来,登录都是 web 系统很基础却很重要的功能,但是其原理,却很少进行梳理。
在做入职测试的时候,再一次遇到了登录的问题,碍于时间紧张,只是简单的进行了实现,而过程中遇到的一些疑惑,自己也没有进行解答。
有空之余便查阅了一些相关文档,结合自己的理解,整理出本文,便于自己翻阅参考, 同时如果之后工作中有更多的领悟,也会更新到本文中。
1. 简单实用的登录
前端输入账号密码,发送请求到服务端
服务端端查询数据库,账号密码匹配则登录成功,服务器端生成并设置 Session
Session[SessionID] = 用户信息
,同时将 SessionID 通过set-cookie: SessionID
头设置到客户端的 cookie 中,这样后续客户端发起的请求,都会带上这个cookie。Session 对象存在于服务端内存中,当服务重启时,Session 失效。当后续请求携带 Cookie 时,服务端验证该 SessionID 是否有效(是否存在且没有过期),如果有效则从 Session 中读取先前保存的 用户信息,以该身份进行之后的请求处理。
缺点: 1. 前面也提到了,Session 对象存在服务器内存中,如果服务器出现异常导致重启,那么用户就可能突然登出。解决的办法是:引入新的存储系统如 redis 等来存储会话信息
问题:
那如果 redis 也出错了怎么办,是不是 redis 要进行灾备,保证两个以上 redis 存在,通过增加数量保证一定有一个服务是好的
注意点:
服务端 Session 和 客户端 Cookie 都可以设置过期时间
2. 传统 Web 应用中身份验证的最佳实践
由于第一种方案需要在服务端保存 Session,那么就会遇到需要 Session 共享的问题,比如负载均衡中,有 A,B,C,D 四个服务器,用户第一次在 A 服务器登录,Session 存在于 A 服务器中,下次如果用户访问到 B 服务器,那么就会被认为是未登录的。
解决办法是: 将登录信息之后的 Session 放在第三方的存储服务中,A.B.C.D 服务器都去访问第三方存储服务校验是否登录。
最佳实践: 参考 JSON Web Token
其实我们可以不把登录信息放在 Session 中,而是直接加密返回给客户端,(通过使用自包含的,含有加密内容的 Cookie 作为替代凭据)。
客户端使用账号密码登录,服务端验证账号密码匹配,则使用加密算法,加密用户信息得到一个字符串,并
set-cookie=该加密字符串
客户端得到该 cookie 之后,以后每次请求带上 cookie,服务端使用解密算法解析出用户的身份信息,进行下一步的请求处理
由于校验的过程是通过加解密算法的配合,只要算法一致,不同的服务器也能得到同样的结果,因此不存在考虑如何共享登录态的问题。
3. 单点登录
用户在一个站点登录之后,不需要在其它兄弟站点中再次登录。比如,假设 在支付宝登录之后,再进入淘宝或者阿里云,不需要再次登录。
流程图如下:
客户端请求应用系统 1,没有登录,于是让客户端会访问独立的鉴权站点: IDB 身份验证系统
客户端在 IDB 身份验证系统登录,且 IDB 身份验证系统返回 ticket 给客户端
之后,客户端访问应用系统 3 的时候,会携带 ticket,而应用系统 3 则拿着 ticket 去 IDB 身份验证系统进行校验。由于客户端已经在 IDB 系统中登录,因此该请求会被允许。从而进行接下来的请求处理流程。
由于没有实践过,单点登录的知识简单整理至此,有必要可以查找相关资料深入学习。回忆下之前上家公司内网系统的 SSO 运用,第一次登录 A 系统会跳转到一个独立的页面进行登录,之后再进入 B 系统,不需要再次输入账号密码,原理估计就是如此吧。
4. 登录实践
在真实项目中,除了登录成功,前端获得 cookie ,再后续请求携带 cookie 之外。还会涉及到路由跳转拦截(比如想直接到一个需要登录的页面应该被拒绝),或者已经登录之后想主动回到登录页也应该是被拒绝,除非主动退出登录删除 cookie。
本章用 Vue-Router + Vuex 举例:
首先,对于我们开发者来说,我们是知道哪些页面需要登录态才能访问,而哪些页面不需要登录也能访问。因此我们可以在路由中添加字段以作区分:
比如我们定义如下几个路由:
如上,有登录页,下载页和 FAQ 页面不需要登录态即可访问,而详情页和列表页则需要登录态。
4.1 客户端如何保存登录态信息
客户端通过账号密码登录成功之后,服务端有可能直接set-cookie=token
设置 cookie 到了前端。也可能是将 token 信息作为返回体进行返回。
无论哪一种方式,针对 vuex 这种方案,我们可以将 token 信息存入到 vuex 的 state 中,通过内存访问 state 比每次通过 API 访问 cookie 或者 locaStorage 应该是更为高效吧(我猜的)。
为了实现关闭浏览器或者刷新浏览器之后,网站不需要再次登录,我们可以使用 cookie 或者 localStorage 来保存 token 信息,因为 vuex 是 基于内存的,刷新或者关闭浏览器都会失效。
这样我们就保存了登录态信息。
4.2 保存登录态信息之后还要做什么
在客户端保存登录态信息之后,在每一次进行页面跳转(或者请求接口之前),都可以进行登录态的校验(只是单纯校验该 token 是否有效,而无法真正校验 token 是否有效,真正的校验还是需要后端)。
如上所说,我们可以拿到 from, to, checkLogged 等信息,来进行判断,那我们来进行穷举吧。
4.3 页面之间跳转的 18 种情况
先规定几种标识:
from: f1 => 当前页面需要登录(如 /list, /detail) f2 => 当前页面不需要登录,但也不是登录页(如 /download, /faq): 之后称为普通页 f3 => 当前页面不需要登录,且是登录页
to: t1 => 想去的页面需要登录(如 /list, /detail) t2 => 想去的页面不需要登录,但也不是登录页(如 /download, /faq):之后称为普通页 t3 => 想去的页面不需要登录,且是登录页
checkLogged(): token0 => 用户已登录 token1 => 用户未登录
情况 | checkLogged | from | to | 描述 | 结果 |
1 | toekn0 | f1 | t1 | 已登录用户,从需要登录页面,跳转到需要登录的页面 | next() 不做处理,放行该跳转 |
2 | token0 | f1 | t2 | 已登录用户,从需要登录的页面,跳转到普通页面 | next() 即不做处理,放行该跳转 |
3 | token0 | f1 | t3 | 已登录用户,从需要登录的页面,跳转到登录页 | next(特定页面) 不允许跳转,提醒“跳转失败,需要先退出登录” |
4 | token0 | f2 | t1 | 已登录用户,从普通页面,跳转到需要登录的页面 | next() 即不作处理,放行该跳转 |
5 | token0 | f2 | t2 | 已登录用户,从普通页面,跳转普通页面 | next() 即不作处理,放行该跳转 |
6 | token0 | f2 | t3 | 已登录用户,从普通页面,跳转登录页面 | next(特定页面) 不允许跳转,提醒“跳转失败,需要先退出登录” |
7 | token0 | f3 | t1 | 已登录用户,从登录页面,跳转到需要登录的页面 | 不考虑这种情况 一个已登录用户,不可能在登录页 |
8 | token0 | f3 | t2 | 已登录用户,从登录页面,跳转到普通页面 | 不考虑这种情况 一个已登录用户,不可能在登录页 |
9 | token0 | f3 | t3 | 已登录用户,从登录页面,跳转登录页面 | 不考虑这种情况 一个已登录用户,不可能在登录页 |
10 | toekn1 | f1 | t1 | 未登录用户,从需要登录页面,跳转到需要登录的页面 | next(登录页) 一开始用户登录了,出现在已登录页面 后来由于登陆态过期,变成未登录用户 此时跳转到需要登录的页面,则需要先跳转到登录页 |
11 | token1 | f1 | t2 | 未登录用户,从需要登录的页面,跳转到普通页面 | next() 即不做处理,放行该跳转,因为普通页面不需要登录态 |
12 | token1 | f1 | t3 | 未登录用户,从需要登录的页面,跳转到登录页 | next() 放行该跳转 |
13 | token1 | f2 | t1 | 未登录用户,从普通页面,跳转到需要登录的页面 | next(登录页) 需要先跳转到登录页面 |
14 | token1 | f2 | t2 | 未登录用户,从普通页面,跳转普通页面 | next() 直接放行 |
15 | token1 | f2 | t3 | 未登录用户,从普通页面,跳转登录页面 | next() 放行” |
16 | token1 | f3 | t1 | 未登录用户,从登录页面,跳转到需要登录的页面 | next(login) 需要先跳转到登录页面 |
17 | token1 | f3 | t2 | 未登录用户,从登录页面,跳转到普通页面 | next() 放行 |
18 | token1 | f3 | t3 | 未登录用户,从登录页面,跳转登录页面 | next() 放行 |
由以上信息可知,虽然一共有18种情况,但是实际可以总结为四种情况 (因为有些信息,比如用户当前页面的信息,其实是没有用到的):
用户已登录
用户想跳转去登录页 => 提醒跳转失败,需要先退出登录才能到登录页面,并跳转到指定页面或不进行跳转。
否则, 直接放行
用户未登录
用户想跳转到需要登录的页面 => 跳转到提醒用户先要进行登录,并跳转到 登录页
否则,直接放行
导航守卫
5. 某公司内部某系统的登录流程梳理
权限系统: 用户,角色,权限(API接口)三者之间互相多对多的关系。
5.1 登录
前端输入账号密码进行登录
node 端得到账号密码之后,用账号密码请求
sso
单点登录接口,返回sso
相关信息(包含用户唯一标识)之后,md5
加密该信息得到token
通过用户唯一标识,查询数据库,得到该用户的详细信息,以及该用户的权限列表。
redis
缓存该用户token
,值为用户详细信息及权限列表。设置一定的有效期。并返回到前端,(如果有需要也可以将不敏感的用户信息进行返回,一般系统都有个人信息的展示)。前端将
token
保存到localStorage
中,封装ajax
方法,每次请求都会带上token
之后每次请求,后端将前端带入的
token
拿来校验,判断是否有效。有效则取出token
中的用户信息和权限列表。再判断用户是否拥有当前请求的权限,有则继续下一步,没有则返回。
5.2 用户退出登录
前端发送退出登录请求,后端执行 redis.remove(token)
清除 token
信息,同时前端清除 localStorage
中的 token
,跳转到登录页。
这样,请求后端接口时,由于没有 token
,后端不会返回数据。
而前端路由方面,可以通过全局的导航守卫设置,localStorage
没有 token
信息,就会跳转到登录页。
5.3 系统管理员强制他人退出登录
系统管理员可以查看到所有用户,可以清楚别人登录的 token
。
原理是,用户在登录的时候,token
( md5
加密的用户信息和时间信息) 除了设置进 redis
, 还会更新到数据库里(如 user
表的 access_token
字段)。
那系统管理员可以知道所有用户的 id
,通过 id
查询到用户的 access_token
值,再执行 redis.remove( access_token )
进行用户 token
的删除。
用户那边下一次请求时,后端校验 token
失败,则前端进行 localStorage token
的删除以及跳转到登录页。
6. 登录与安全
刚刚听了部门安全的分享,发现在记录这部分知识的时候忽略了安全,如果完全依赖于 cookie 判断登录, 那么就需要防范 CSRF 的攻击。
依赖 localStorage, 也可能被 xss 攻击,暴露 localStorage。
这样,登录问题又变得复杂了起来。
参考资料
ThoughtWorks: 登录工程:传统 Web 应用中的身份验证技术
阮一峰: JSON Web Token 入门教程
Last updated