「LeanCloud Web 应用开发实践」系列直播及文章分享持续进行中。
每周二周四晚上 8 点开始,时长预计 45 分钟。在 “leanCloud通信” 微信公众号回复 “公开课” 便可获取直播连接。html
《LeanCloud Web 应用开发实践公开课》上期回顾和本期主题介绍。前端
点击查看完整公开课视频node
为了理清 currentUser 的状态,须要看下不一样类型的 WEB 应用是如何运做的。git
使用云引擎 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>复制代码
先配置云引擎 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。浏览器
整个请求和响应的流程:
浏览器并提交表单的 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)复制代码
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'
<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 输出一些日志。
使用浏览器请求 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 的先后端分离应用,运行应用的域(好比云引擎的二级域名 abc.leanapp.cn )和提供服务的域(好比 LeanCloud 存储服务 api.leancloud.cn/1.1/class/T… )不一样,根据 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 拒绝操做或返回空结果。