「LeanCloud Web 应用开发实践」系列直播及文章分享持续进行中。
每周二周四晚上 8 点开始,时长预计 45 分钟。在 “leanCloud通讯” 微信公众号回复 “公开课” 即可获取直播链接。
《LeanCloud Web 应用开发实践公开课》上期回顾和本期主题介绍。
点击查看完整公开课视频
为了理清 currentUser 的状态,需要看下不同类型的 WEB 应用是如何运作的。
使用云引擎 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>
先配置云引擎 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。
整个请求和响应的流程:
curl -v 'https://todo-demo.leanapp.cn/users/login' -H 'content-type: application/x-www-form-urlencoded' --data 'username=zhangsan&password=zhangsan'
var username = req.body.username;
var password = req.body.password;
AV.User.logIn(username, password)
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
获取发起请求的登录用户信息了。
对于服务端渲染的应用:
服务端渲染的应用在用户体验方面存在不足,比如一系列表单填写完成之后一次性提交,此时服务端判断参数是否有效再响应用户;还有服务端每次响应整个 HTML 有很大的带宽浪费。之后出现了 AJAX 技术使得光标离开某个表单项之后,浏览器单独发送请求到服务端直接判断其有效性并迅速响应;并且每次浏览器与服务端通信都是一些数据结构(JSON 或者 XML)来降低流量,浏览器根据数据结果来修改 DOM 结构进行展现。
LeanCloud 将存储服务以 REST API 的方式提供服务,让前端(浏览器,或移动设备)可以方便的操作数据,这使得基于 LeanCloud 的应用基本都是前后端分离的。
当前示例使用一些简单页面来模拟前后端分离的应用。
请求一个前后端分离的示例(页面代码):
$ curl 'https://todo-demo.leanapp.cn/static/page1.html'
page1
服务端响应了一个页面,浏览器渲染页面时,会执行 script 部分的脚本,该脚本可能会做大量工作,比如生成或者修改页面 DOM,并向服务器发请求获取其他数据。比如这个示例就在页面打开之后 3 秒,通过 JS SDK 向服务器发起一个用户登录的请求,收到响应后在浏览器 console 输出一些日志。
使用浏览器请求 page1 ,整个流程如下:
var APP_ID = 'kdrt5GNCjojUjiIujawd5A4n-gzGzoHsz';
var APP_KEY = 'Xvxjo6SVUITIqet69q3mudlF';
AV.init({
appId: APP_ID,
appKey: APP_KEY
});
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'))
...
云函数 是运行在云引擎(服务端)的一个方法,通过 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);
})
...
浏览器请求云函数流程如下:
通过 JS SDK 调用云函数,并根据需要传递参数(示例中未涉及)。JS SDK 会根据 Local Storage 中的信息在请求的 header 中附加 X-LC-Session ,值为用户身份标示 sessionToken。
请求到达云引擎应用,云引擎中间件会判断是否存在 X-LC-Session 的信息,如果有,就使用该值通过存储服务获取用户信息,并赋值给 request.currentUser。
请求进入云函数相关代码流程,开发者就可以获取到 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 中携带信息让服务端感知到当前登录用户。
基于 LeanCloud 的前后端分离应用:
登录方式 | 云引擎自定义路由 | 浏览器 JS SDK + REST API(云函数) |
---|---|---|
保存位置 | cookie | Local Storage |
服务端感知方式 | 通过 cookieSession 中间件 从 cookie 获取 | 通过云引擎中间件从 header 获取 |
与服务端交互方式 | 页面跳转或表单提交。因为同域,cookie 自动携带 | 通过 JS SDK 操作存储服务的数据或调用云函数。因为跨域,cookie 无法携带,使用 header。 |
服务端用户登录/登出操作 | 自定义路由中用户登录/登出后可以操作相关 cookie,浏览器 cookie 更新,影响后续请求。 | 云函数中用户登录/登出没有意义,不会改变浏览器 Local Storage 的内容,不影响后续浏览器对云函数的请求。 |
相信到这里,最初提出的疑问可以解释了:
在云引擎登录了,但是云函数却没有 currentUser
云引擎自定义路由登录只改变浏览器 cookie,而后续在浏览器通过 JS SDK 调用云函数时,是否携带 SessionToken
的信息在 header
中,和 cookie 无关。
在浏览器调用 JS SDK 登录用户,页面跳转时云引擎中没有 currentUser
浏览器调用 JS SDK 用户登录相关的 API 之后,只是 Local Storage
有变化,并在之后的访问存储服务或云函数时会将 sessionToken
携带在 header
中,cookie 并无变化。而应用页面跳转,或者 form 表单提交访问云引擎自定义路由时, cookieSession 中间件 无法从 cookie 中获取需要的信息。
sessionToken
。sessionToken
,并调用 JS SDK 的 AV.User.become
方法在浏览器登录。在此之后,不管是请求云引擎自定义路由还是请求云函数,都能确保 currentUser 的存在。当然 cookie 还存在过期的问题,不过这里就不展开讨论了。
通过控制云引擎中间件的 fetchUser 属性,可以降低一部分不必要的 _User
的查询请求。
以 AV.Cloud.define API 为例,当收到云函数请求时,云引擎中间件从请求 header
中获取 sessionToken
信息,并且确认下 fetchUser
属性的值:
sessionToken
从存储服务读取用户(_User
表)的信息。之后将 sessionToken
和 currentUser
信息复制到 request
的相关属性上。sessionToken
赋值到 request 的属性上。也就意味着云函数中 ```request.currentUser
为 undefined
。如果云函数的相关逻辑需要 _User
的其他信息,比如 username
,那就设置 fetchUser
为 true
,或者不设置使其保持默认值。
否则,可以设置 fetchUser
为 false
,但是需要在所有数据操作(和云函数调用)时将 sessionToken 加入到请求中:
var query = new AV.Query('Todo');
query.equalTo('status', 0);
query.find({sessionToken: req.sessionToken})
如果 req.sessionToken 有效,则存储服务会根据查询条件和 ACL 返回适当的信息。
如果 req.sessionToken 无效(过期或伪造),则存储服务可能因为 ACL 拒绝操作或返回空结果。