幫你理清 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 拒絕操做或返回空結果。

相關文章
相關標籤/搜索