帮你理清 Web 应用的登录状态_leancloud web请求-程序员宅基地

技术标签: 应用开发  web  视频  acl  后端开发  

「LeanCloud Web 应用开发实践」系列直播及文章分享持续进行中。

每周二周四晚上 8 点开始,时长预计 45 分钟。在 “leanCloud通讯” 微信公众号回复 “公开课” 即可获取直播链接。

《LeanCloud Web 应用开发实践公开课》上期回顾和本期主题介绍。

点击查看完整公开课视频

抛出疑问 00:01:10

  • 在云引擎登录了,但是云函数却没有 currentUser
  • 在浏览器调用 JS SDK 登录用户,页面跳转时云引擎中没有 currentUser
  • 云引擎 SDK 中有些地方会有 fetchUser 属性,有什么用?

为了理清 currentUser 的状态,需要看下不同类型的 WEB 应用是如何运作的。

早期 WEB 应用——服务端渲染 00:02:40

使用云引擎 demo 来演示,可以使用 https://todo-demo.leanapp.cn 来做接下来的尝试,或者自己部署该 demo 应用尝试(代码 版本: 1efc44a )。

这个 demo 是一个典型的服务端渲染的应用。所谓的服务端渲染是指浏览器请求服务端的地址或资源时,服务端返回一个 HTML 文档(一个很大的字符串),浏览器收到 HTML 文档之后,进行渲染并呈现页面。通过云引擎的自定义路由很容易实现这样的 WEB 应用。

如果单纯看请求和响应,以登录页面为例:

$ curl -v https://todo-demo.leanapp.cn/users/login
> GET /users/login HTTP/1.1
> Host: todo-demo.leanapp.cn
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
<
<!DOCTYPE html><html><head><title>用户登录</title>...<input type="submit" 
value="登录" class="btn btn-default"><a href="/users/register" class="btn btn-default">注册</a></div></form></div></body></html> 
提示:为了方便表达,所有页面请求都转化为 curl 请求的方式,下同。
提示:为了节省空间,删掉了很多额外的内容(下同),可以自己执行 curl 命令看完整结果。

服务端如何感知登录用户? 00:07:41

提示:请勾选浏览器控制台 Network 标签页的 Preserve log 选项,这样之前的请求在页面跳转之后还会保留,方便观察。

先配置云引擎 cookieSession中间件代码):

app.use(AV.Cloud.CookieSession({ secret: '05XgTktKPMkU', maxAge: 3600000, fetchUser: true }));

用户登录路由的 代码 如下:

router.post('/login', function(req, res, next) {
    
  var username = req.body.username;
  var password = req.body.password;
  AV.User.logIn(username, password).then(function(user) {
    
    res.saveCurrentUser(user);
    res.redirect('/todos');
  }, function(err) {
    
    res.redirect('/users/login?errMsg=' + err.message);
  }).catch(next);
});

在云引擎的自定义路由中调用了 AV.User.logIn 的 API,并且调用了 res.saveCurrentUser(user); 来将用户信息写入 cookie。

整个请求和响应的流程:

  • 浏览器并提交表单的 username 和 password 信息,向服务器发起请求:
curl -v 'https://todo-demo.leanapp.cn/users/login' -H 'content-type: application/x-www-form-urlencoded' --data 'username=zhangsan&password=zhangsan'
  • 请求到达云引擎登录相关的路由,根据 username 和 password 进行登录:
var username = req.body.username;
var password = req.body.password;
AV.User.logIn(username, password)
  • 路由方法将用户信息写入 cookie:
res.saveCurrentUser(user);

该操作在最终请求响应时, cookieSession 中间件 会将用户的信息写入 header 的 Set-Cookie 中。

  • 浏览器收到响应:
< HTTP/1.1 302 Found
< Content-Type: text/plain; charset=utf-8
< Location: /todos
< Set-Cookie: avos:sess=eyJfdWlkIjoiNTUxZDJkZTZlNGIwYjM2NzFhZWNmZWIyIiwiX3Nlc3Npb25Ub2tlbiI6ImFjajd3eTgwdDhmdGtpYzRxYzY1ZDNiZDgifQ==; path=/; expires=Tue, 08 Aug 2017 15:49:21 GMT; secure; httponly
< Set-Cookie: avos:sess.sig=TyI_sXTvNa4nUSxByoX3zxWRZ8M; path=/; expires=Tue, 08 Aug 2017 15:49:21 GMT; secure; httponly
<

在响应里多了两个 Set-Cookie信息,收到这样的响应后,浏览器会在 cookie 里写入这些信息,其中 avos:sess对应的值是一个 base64 字符串,具体内容是 :

{"uid":"551d2de6e4b0b3671aecfeb2","sessionToken":"acj7wy80t8ftkic4qc65d3bd8"}

所以标示用户身份的 sessionToken 信息保存在 cookie 里。

提示:avos:sess.sig 是一个校验使用字符串,可以不关心。

cookie 有个特性:每次请求服务器时,会把 cookie 自动添加到请求的 header 中。所以之后再请求该站点的其他页面:

curl 'https://todo-demo.leanapp.cn/todos' -H 'cookie: avos:sess=eyJfdWlkIjoiNTUxZDJkZTZlNGIwYjM2NzFhZWNmZWIyIiwiX3Nlc3Npb25Ub2tlbiI6ImFjajd3eTgwdDhmdGtpYzRxYzY1ZDNiZDgifQ==; avos:sess.sig=TyI_sXTvNa4nUSxByoX3zxWRZ8M'

当这些请求到达云引擎应用之后, cookieSession 中间件 会再次起作用,从请求 header 中取出相关的 cookie 并校验,从中能获取到登录用户的 sessionToken ,然后从存储服务获取该用户的信息(或称为判断 sessionToken 是否有效),并将 user 信息赋值到 request.currentUser 属性上。

之后,请求会到达具体的自定义路由,此时就可以从 request.currentUser 获取发起请求的登录用户信息了。

小结 00:20:20

对于服务端渲染的应用:

  • 服务端响应整个 HTML,浏览器负责渲染并展现
  • 浏览器提交账号密码,服务端进行用户登录,并把代表用户身份的标示(比如 sessionToken)保存到 cookie 中。
  • 浏览器会保存服务端返回的 cookie,并在之后的请求中携带这些 cookie。
  • 服务端根据每次请求的 cookie 信息中判断是否有用户身份标示,并确认本次请求是否存在一个「当前登录用户」。

前后端分离的应用 00:22:10

服务端渲染的应用在用户体验方面存在不足,比如一系列表单填写完成之后一次性提交,此时服务端判断参数是否有效再响应用户;还有服务端每次响应整个 HTML 有很大的带宽浪费。之后出现了 AJAX 技术使得光标离开某个表单项之后,浏览器单独发送请求到服务端直接判断其有效性并迅速响应;并且每次浏览器与服务端通信都是一些数据结构(JSON 或者 XML)来降低流量,浏览器根据数据结果来修改 DOM 结构进行展现。

LeanCloud 将存储服务以 REST API 的方式提供服务,让前端(浏览器,或移动设备)可以方便的操作数据,这使得基于 LeanCloud 的应用基本都是前后端分离的。

当前示例使用一些简单页面来模拟前后端分离的应用。

前后端分离应用的请求 00:24:35

请求一个前后端分离的示例(页面代码):

$ curl 'https://todo-demo.leanapp.cn/static/page1.html'
<html>
  <head>
    <script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
  </head>
  <body>
    <h1>page1</h1>
    <script>
      ...
        console.log('当前登录用户:%s', AV.User.current() && AV.User.current().get('username'))

        console.log('开始登录...')
        AV.User.logIn('zhangsan', 'zhangsan')
        .then(function(user) {
          console.log('登录成功: username: %s, sessionToken: %s', user.get('username'), user._sessionToken)
        })
        .then(function() {
          console.log('当前登录用户:%s', AV.User.current() && AV.User.current().get('username'))
      ...
    </script>
  </body>
</html>

服务端响应了一个页面,浏览器渲染页面时,会执行 script 部分的脚本,该脚本可能会做大量工作,比如生成或者修改页面 DOM,并向服务器发请求获取其他数据。比如这个示例就在页面打开之后 3 秒,通过 JS SDK 向服务器发起一个用户登录的请求,收到响应后在浏览器 console 输出一些日志。

提示:浏览器中可能会出现一些 OPTIONS 请求,具体原因见 HTTP访问控制(CORS)

使用浏览器请求 page1 ,整个流程如下:

  • 页面被渲染完成之后,也一起完成了 AV 对象的初始化工作。
var APP_ID = 'kdrt5GNCjojUjiIujawd5A4n-gzGzoHsz';
var APP_KEY = 'Xvxjo6SVUITIqet69q3mudlF';
AV.init({
 appId: APP_ID,
 appKey: APP_KEY
});
  • 3 秒之后,页面脚本通过 JS SDK 的 AV.User.logIn 方法向 LeanCloud 服务器发起登录请求。
setTimeout(function() {
 console.log('当前登录用户:%s', AV.User.current() && AV.User.current().get('username'))
 console.log('开始登录...')
 AV.User.logIn('zhangsan', 'zhangsan')
}, 3000)
  • 服务器响应用户信息:
{
 "sessionToken": "u2xtq3dxxvonapqn5uc9snbz7",
 "updatedAt": "2017-08-07T14:39:07.619Z",
 "objectId": "59887b8b570c350062430143",
 "username": "zhangsan",
 "createdAt": "2017-08-07T14:39:07.619Z",
 "emailVerified": false,
 "mobilePhoneVerified": false
}

JS SDK 将该信息反序列化构造出AV.User 对象,然后将其保存在浏览器 Local Storage 中。

通过 JS SDK 的 AV.User.current() 方法获取当前登录用户,本质上就是去 Local Storage 获取用户的信息并返回调用方(比如请求 page2页面代码):

...
console.log('当前登录用户:%s', AV.User.current() && AV.User.current().get('username'))
...
服务端如何感知登录用户 00:34:00

云函数 是运行在云引擎(服务端)的一个方法,通过 JS SDK 的 AV.Cloud.run 方法可以很方便的调用。

示例中定义了一个云函数(代码):

...
AV.Cloud.define('whoami', function(req, res) {
  console.log('whoami:', req.currentUser);
  var username = req.currentUser && req.currentUser.get('username');
  res.success(username);
});
...

在浏览器中通过 JS SDK 调用云函数(请求 page3页面代码):

...
AV.Cloud.run('whoami')
.then(function(username) {
  console.log('whoami:', username);
})
...

浏览器请求云函数流程如下:

  1. 通过 JS SDK 调用云函数,并根据需要传递参数(示例中未涉及)。JS SDK 会根据 Local Storage 中的信息在请求的 header 中附加 X-LC-Session ,值为用户身份标示 sessionToken。

  2. 请求到达云引擎应用,云引擎中间件会判断是否存在 X-LC-Session 的信息,如果有,就使用该值通过存储服务获取用户信息,并赋值给 request.currentUser。

  3. 请求进入云函数相关代码流程,开发者就可以获取到 currentUser 了:

console.log('whoami:', req.currentUser);
var username = req.currentUser && req.currentUser.get('username');
res.success(username);

因为使用 LeanCloud 的前后端分离应用,运行应用的域(比如云引擎的二级域名 http://abc.leanapp.cn )和提供服务的域(比如 LeanCloud 存储服务 https://api.leancloud.cn/1.1/class/Todo )不同,根据 cookie 的安全策略是不能在不同域传递 cookie 的。

所以 LeanCloud 的 SDK 会在请求的 header 中携带信息让服务端感知到当前登录用户。

小结 00:55:13

基于 LeanCloud 的前后端分离应用:

  • 使用云引擎返回「初始化状态」页面。
  • 浏览器通过 js 脚本决定如何渲染页面,经常是单页面应用。
  • 与服务端交互通过 REST API:由 JS SDK 封装,数据操作走存储服务,云函数操作走云引擎。
  • 因为 WEB 应用的域和服务端的域不同,用户状态不能通过 cookie 传递,而是通过请求 header 传递。

两种方式的对比 00:57:52

登录方式 云引擎自定义路由 浏览器 JS SDK + REST API(云函数)
保存位置 cookie Local Storage
服务端感知方式 通过 cookieSession 中间件 从 cookie 获取 通过云引擎中间件从 header 获取
与服务端交互方式 页面跳转或表单提交。因为同域,cookie 自动携带 通过 JS SDK 操作存储服务的数据或调用云函数。因为跨域,cookie 无法携带,使用 header。
服务端用户登录/登出操作 自定义路由中用户登录/登出后可以操作相关 cookie,浏览器 cookie 更新,影响后续请求。 云函数中用户登录/登出没有意义,不会改变浏览器 Local Storage 的内容,不影响后续浏览器对云函数的请求。

疑问解释 01:10:20

相信到这里,最初提出的疑问可以解释了:

  • 在云引擎登录了,但是云函数却没有 currentUser
    云引擎自定义路由登录只改变浏览器 cookie,而后续在浏览器通过 JS SDK 调用云函数时,是否携带 SessionToken 的信息在 header 中,和 cookie 无关。

  • 在浏览器调用 JS SDK 登录用户,页面跳转时云引擎中没有 currentUser
    浏览器调用 JS SDK 用户登录相关的 API 之后,只是 Local Storage 有变化,并在之后的访问存储服务或云函数时会将 sessionToken 携带在 header 中,cookie 并无变化。而应用页面跳转,或者 form 表单提交访问云引擎自定义路由时, cookieSession 中间件 无法从 cookie 中获取需要的信息。

服务端客户端用户感知同步 01:12:52

登录流程
  1. 浏览器调用服务端登录相关的路由,路由中登录用户,并更新 cookie,且响应中携带 sessionToken
  2. 浏览器收到登录响应,解析出 sessionToken,并调用 JS SDK 的 AV.User.become 方法在浏览器登录。

在此之后,不管是请求云引擎自定义路由还是请求云函数,都能确保 currentUser 的存在。当然 cookie 还存在过期的问题,不过这里就不展开讨论了。

登出流程
  1. 浏览器调用服务端登出路由,该路由可能做一些用户相关的资源清理,并清空 cookie。
  2. 浏览器受到登出响应后,调用 JS SDK 的相关方法在浏览器登出。

fetchUser 属性的作用 01:25:10

通过控制云引擎中间件的 fetchUser 属性,可以降低一部分不必要的 _User 的查询请求。

AV.Cloud.define API 为例,当收到云函数请求时,云引擎中间件从请求 header 中获取 sessionToken 信息,并且确认下 fetchUser 属性的值:

  • 如果为 true (默认):则使用 sessionToken从存储服务读取用户(_User 表)的信息。之后将 sessionTokencurrentUser 信息复制到 request 的相关属性上。
  • 如果为 false:则跳过从存储服务读取用户信息的步骤,只将 sessionToken 赋值到 request 的属性上。也就意味着云函数中 ```request.currentUserundefined
如何判断是否需要设置 fetchUser 的属性 01:33:00
  • 如果云函数的相关逻辑需要 _User 的其他信息,比如 username,那就设置 fetchUsertrue ,或者不设置使其保持默认值。

  • 否则,可以设置 fetchUserfalse ,但是需要在所有数据操作(和云函数调用)时将 sessionToken 加入到请求中:

var query = new AV.Query('Todo');
query.equalTo('status', 0);
query.find({sessionToken: req.sessionToken})

如果 req.sessionToken 有效,则存储服务会根据查询条件和 ACL 返回适当的信息。

如果 req.sessionToken 无效(过期或伪造),则存储服务可能因为 ACL 拒绝操作或返回空结果。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sinat_16793755/article/details/77482146

智能推荐

tkinter创建子窗口(只创建一个)_tkinter子窗口-程序员宅基地

文章浏览阅读635次。【代码】tkinter创建子窗口(只创建一个)_tkinter子窗口

基于微信小程序的小说阅读系统+vue.js附带文章和源代码设计说明文档ppt_微信小程序书代码-程序员宅基地

文章浏览阅读345次,点赞9次,收藏6次。博主介绍:CSDN特邀作者、985计算机专业毕业、某互联网大厂高级全栈开发程序员、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序、前端、python等技术领域和毕业项目实战,以及程序定制化开发、全栈讲解、就业辅导、面试辅导、简历修改。精彩专栏 推荐订阅2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选题推荐2023-2024年最值得选的Java毕业设计选题大全:500个热门选题推荐。_微信小程序书代码

没有U盘Win10电脑下如何使用本地硬盘安装Ubuntu20.04(单双硬盘都行)_没有u盘怎么装ubuntu-程序员宅基地

文章浏览阅读3.6k次,点赞2次,收藏2次。DELL7080台式机两块硬盘。_没有u盘怎么装ubuntu

【POJ 3401】Asteroids-程序员宅基地

文章浏览阅读32次。题面Bessie wants to navigate her spaceship through a dangerous asteroid field in the shape of an N x N grid (1 <= N <= 500). The grid contains K asteroids (1 <= K <= 10,000), which are conv...

工业机器视觉系统的构成与开发过程(理论篇—1)_工业机器视觉系统的构成与开发过程(理论篇—1-程序员宅基地

文章浏览阅读2.6w次,点赞21次,收藏112次。机器视觉则主要是指工业领域视觉的应用研究,例如自主机器人的视觉,用于检测和测量的视觉系统等。它通过在工业领域将图像感知、图像处理、控制理论与软件、硬件紧密结合,并研究解决图像处理和计算机视觉理论在实际应用过程中的问题,以实现高效的运动控制或各种实时操作。_工业机器视觉系统的构成与开发过程(理论篇—1

plt.legend的用法-程序员宅基地

文章浏览阅读5.9w次,点赞32次,收藏58次。legend 传奇、图例。plt.legend()的作用:在plt.plot() 定义后plt.legend() 会显示该 label 的内容,否则会报error: No handles with labels found to put in legend.plt.plot(result_price, color = 'red', label = 'Training Loss') legend作用位置:下图红圈处。..._plt.legend

随便推点

强强联手:强网杯LongTimeAgo复盘分析网络安全-程序员宅基地

文章浏览阅读141次。LongTimeAgo题目是一道备受关注的CTF(Capture The Flag)题目,旨在考察参赛选手在网络安全方面的技能和知识。该题目主要涉及密码学和逆向工程的内容,要求选手通过分析给定的程序,找到隐藏的漏洞并获取关键信息。该题目主要涉及到密码学和逆向工程的内容,需要选手通过分析给定的程序,找到隐藏的漏洞并获取关键信息。观察源代码中的加密函数,我们可以发现密钥的长度是可变的,它取决于输入明文的长度。观察源代码中的加密函数,我们可以发现密钥的长度是可变的,它取决于输入明文的长度。是当前明文字符的索引。_强网杯

【嵌入式总复习】Linux进程间通信_嵌入式 linux 跨进程 信号-程序员宅基地

文章浏览阅读289次。进程间通信简称IPC(interprocess communication),是指在不同进程间传播或交换信息。一个进程需要另一个或另一组进程发送消息,通知它(们)发生了某种事件;一个进程需要将它的数据发送给另一个进程;,信号量,共享内存,消息队列和套接字。多个进程之间共享同样的资源;一个进程完全控制另一个进程;传统的UNIX进程间通信机制。System V IPC机制。System V 消息队列。System V 共享内存。System V 信号量。POSIX 消息队列。POSIX 共享内存。_嵌入式 linux 跨进程 信号

发行版Linux和麒麟操作系统下netperf 网络性能测试-程序员宅基地

文章浏览阅读175次。Netperf是一种网络性能的测量工具,主要针对基于TCP或UDP的传输。Netperf根据应用的不同,可以进行不同模式的网络性能测试,即批量数据传输(bulk data transfer)模式和请求/应答(request/reponse)模式。工作原理Netperf工具以client/server方式工作。server端是netserver,用来侦听来自client端的连接,c..._netperf 麒麟

万字长文详解 Go 程序是怎样跑起来的?| CSDN 博文精选-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏3次。作者| qcrao责编 | 屠敏出品 | 程序员宅基地刚开始写这篇文章的时候,目标非常大,想要探索 Go 程序的一生:编码、编译、汇编、链接、运行、退出。它的每一步具体如何进行,力图弄清 Go 程序的这一生。在这个过程中,我又复习了一遍《程序员的自我修养》。这是一本讲编译、链接的书,非常详细,值得一看!数年前,我第一次看到这本书的书名,就非常喜欢。因为它模仿了周星驰喜剧..._go run 每次都要编译吗

C++之istringstream、ostringstream、stringstream 类详解_c++ istringstream a >> string-程序员宅基地

文章浏览阅读1.4k次,点赞4次,收藏2次。0、C++的输入输出分为三种:(1)基于控制台的I/O (2)基于文件的I/O (3)基于字符串的I/O 1、头文件[cpp] view plaincopyprint?#include 2、作用istringstream类用于执行C++风格的字符串流的输入操作。 ostringstream类用_c++ istringstream a >> string

MySQL 的 binglog、redolog、undolog-程序员宅基地

文章浏览阅读2k次,点赞3次,收藏14次。我们在每个修改的地方都记录一条对应的 redo 日志显然是不现实的,因此实现方式是用时间换空间,我们在数据库崩了之后用日志还原数据时,在执行这条日志之前,数据库应该是一个一致性状态,我们用对应的参数,执行固定的步骤,修改对应的数据。1,MySQL 就是通过 undolog 回滚日志来保证事务原子性的,在异常发生时,对已经执行的操作进行回滚,回滚日志会先于数据持久化到磁盘上(因为它记录的数据比较少,所以持久化的速度快),当用户再次启动数据库的时候,数据库能够通过查询回滚日志来回滚将之前未完成的事务。_binglog

推荐文章

热门文章

相关标签