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

「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'

  
    
  
  
    

page1

服务端响应了一个页面,浏览器渲染页面时,会执行 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 拒绝操作或返回空结果。

你可能感兴趣的:(应用开发,web,视频,后端开发,acl)