帮你理清 Web 应用的登陆状态

「LeanCloud Web 应用开发实践」系列直播及文章分享持续进行中。
每周二周四晚上 8 点开始,时长预计 45 分钟。在 “leanCloud通信” 微信公众号回复 “公开课” 便可获取直播连接。html

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

点击查看完整公开课视频node

抛出疑问 00:01:10

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

为了理清 currentUser 的状态,须要看下不一样类型的 WEB 应用是如何运做的。git

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

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

这个 demo 是一个典型的服务端渲染的应用。所谓的服务端渲染是指浏览器请求服务端的地址或资源时,服务端返回一个 HTML 文档(一个很大的字符串),浏览器收到 HTML 文档以后,进行渲染并呈现页面。经过云引擎的自定义路由很容易实现这样的 WEB 应用。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中间件代码):api

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。浏览器

整个请求和响应的流程:

  1. 浏览器并提交表单的 username 和 password 信息,向服务器发起请求:

    curl -v 'https://todo-demo.leanapp.cn/users/login' -H 'content-type: application/x-www-form-urlencoded' --data 'username=zhangsan&password=zhangsan'复制代码
  2. 请求到达云引擎登陆相关的路由,根据 username 和 password 进行登陆:

    var username = req.body.username;
    var password = req.body.password;
    AV.User.logIn(username, password)复制代码
  3. 路由方法将用户信息写入 cookie:
    res.saveCurrentUser(user);复制代码

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

  1. 浏览器收到响应:
    < 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 ,整个流程以下:

  1. 页面被渲染完成以后,也一块儿完成了 AV 对象的初始化工做。
    var APP_ID = 'kdrt5GNCjojUjiIujawd5A4n-gzGzoHsz';
    var APP_KEY = 'Xvxjo6SVUITIqet69q3mudlF';
    AV.init({
    appId: APP_ID,
    appKey: APP_KEY
    });复制代码
  2. 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)复制代码
  3. 服务器响应用户信息:
    {
    "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 的先后端分离应用,运行应用的域(好比云引擎的二级域名 abc.leanapp.cn )和提供服务的域(好比 LeanCloud 存储服务 api.leancloud.cn/1.1/class/T… )不一样,根据 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 拒绝操做或返回空结果。

相关文章
相关标签/搜索