魚塘翻了,記Node中通過redis快取session資訊遇到的坑

彭道寬發表於2019-03-22

前戲得做好

大哥們,小弟我又來了,這次真的是請求幫助的了,先說一下,這是第一次用 Node + Express + Mysql 來擼一個專案的後端,正因為第一次,所以遇到了不少的問題,? 本文除了記錄一下,最後還是想讓各位大哥給我提供一些方案解決,我...我在這先謝過了

說一下場景,其實就是一個簡單的功能,是這樣的 ?

  • 使用者登陸,輸入郵箱,點選 ? 獲取驗證碼, 傳送請求

  • 後端通過 nodemailer 給郵箱傳送驗證碼

  • 傳送成功,session 快取這個code

  • 使用者輸入使用者名稱、密碼、郵箱、驗證碼,進行登陸

  • 檢驗 req.body.code == session.get('code')

  • 相同進行 sql 查詢使用者資訊不同告知使用者驗證碼不正確

看似簡單,實際上...? 真的簡單,但是臣妾真的不會啊。。

下面就開始講講我苦逼的搬磚過程

搬磚辛酸史

前端程式碼就不用說了,就是一個按鈕 ?,點選之後傳送請求...

/**
 * @desc: 根據emai傳送驗證碼
 * @return {*}
 */
retrieveCode: email => {
  return request({
    url: `${baseUrl}/api/login/email-code`,
    method: 'POST',
    data: {
      email: email
    }
  })
}
複製程式碼

ojbk,穩重,然後在 Node 後端中,盤它

/**
 * @desc 根據email傳送驗證碼
 * @param {String} email
 */
router.post('/email-code', async (req, res) => {
  try {
    const response = await loginController.retrieveCode(req, req.body)
    res.json(response)
  } catch (err) {
    throw new Error(err)
  }
})
複製程式碼

到這裡應該都沒問題,呼叫 loginController.retrieveCode() 去做處理,然後在裡邊我們應該傳送驗證碼,對吧~然後通過 express-session 快取 code 到 session 中,讓我們看看程式碼

const types = require('../../utils/error.code')
const stmp = require('../../config/smtp')
/**
 * @desc 通過email傳送驗證碼
 * @params {email} 郵箱
 * @return {Object}
 */
async function retrieveCode(req, payload) {
  try {
    var code = ''
    while (code.length < 5) {
      code += Math.floor(Math.random() * 10)
    }

    var emailOptions = stmp.setMailOptions(payload.email, 'code', code)
    await stmp.transporter.sendMail(emailOptions)

    if (!req.session) {
      return next(new Error('oh no')) // handle error
    } else {
      req.session.email_code = code
      console.log('列印本次的req', req)
    }
    return {
      code: types.login.RETRIEVE_EMAIL_CODE_SUCCESS,
      msg: '驗證碼傳送成功~',
      data: null
    }
  } catch (error) {
    return {
      code: types.login.RETRIEVE_EMAIL_CODE_FAIL,
      msg: '驗證碼傳送錯誤, 請檢驗郵箱正確性',
      data: null
    }
  }
}
複製程式碼

程式碼不是什麼神仙程式碼,都能看得懂,重點來了,我在 req.session 中存了這個 email_code,然後呢,我列印了 console.log(req.session),發現是這樣的

  console.log('列印本次的req', req)
  // 下面是列印結果, 其他部分剔除
  sessionID: 'Y11FsZ0vgcJFPyJIftuEItLQn8P4rVg-',
    session:
     Session {
       cookie:
        { path: '/',
          _expires: null,
          originalMaxAge: null,
          httpOnly: true },
       email_code: '71704' }, // 看到了嗎,快取了,真開心!!!
複製程式碼

? 愛情來的像龍捲風

內心 OS : ? 真開心,一點難度都沒有嘛,沖沖衝!?

不到1分鐘,真香,呵,是我年輕了,沒錯,上邊的session中確實是快取了 email_code,但是在下一個請求中,死活就是獲取不到 session 快取的 email_code

/**
 * @desc 獲取token
 * @return {Object}
 */
async function retrieveToken(req) {
  // 1. 先獲取 session 快取的 email_code
  // 2. 與req.body.code 進行比較
  console.log(req.session.email_code) // undefined
  console.log('siri, 給我列印這次的req', req)
}
複製程式碼

yes,沒錯,就是 undefined,奇了怪了,為什麼沒有呢?於是我把這次的 req 列印出來,是這樣的

console.log('siri, 給我列印這次的req', req)
// 下面是列印結果
sessionID: 'MvoJQR8BSQZA6zcfuJFYuJltQH5ZU1rS',
  session:
    Session {
      cookie:
      { path: '/',
        _expires: null,
        originalMaxAge: null,
        httpOnly: true } },
  ...
複製程式碼

看到了嗎,sessionID都不一樣了,呵,玩我呢?於是我就在想,是為什麼,難道,? 是我太騷了??於是開始排查問題...

坑還是得一步一步填

因為用的是 express-session 去操作的,所以當然第一步是去 github 看看文件啦~

在 github 看了一下 README 文件,發現了一句話

Please note that secure: true is a recommended option. However, it requires an https-enabled website, i.e., HTTPS is necessary for secure cookies. If secure is set, and you access your site over HTTP, the cookie will not be set. If you have your node.js behind a proxy and are using secure: true, you need to set "trust proxy" in express:

不用我翻譯了吧,大概意思就是 如果啟用了 secure,但是是用 HTTP 進行的訪問,那麼 cookie 不會傳送給客戶端

也就是說,如果你採用 http 訪問,那麼你的 secure 應該設為 false

然後我百度了一下,發現不下 20 篇文章,都是這樣配置,然後就設定值,再取值

var express = require('express')
var app = express()
var session = require('express-session')

app.use(
  session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
    cookie: {
      maxAge: 60000,
      secure: false
    }
  })
)

// 設定值
req.session.user_id = req.body.user_id

// 取值
const user_id = req.session.user_id
複製程式碼
魚塘翻了,記Node中通過redis快取session資訊遇到的坑

你們看看我的,我是後媽生的?為什麼我的就是不對呢?

// 存session,正常可以存
async function retrieveCode(req, payload) {
  // code...
  req.session.email_code = code
  console.log('快取code : ', req.session.email_code) // 快取code 49167
  // ...
}

// 取session,取不到
async function retrieveToken(req) {
  // code...
  console.log('從快取session中取code : ', req.session.email_code) // undefined

  // ...
}
複製程式碼

累覺不愛

百思不得其解,然後百度的那些二三十篇文章,臥槽,? 怎麼都長的一模一樣,千篇一律,底部就都掛著 原文連結友情連結大哥們,你們這樣真的好嗎???

魚塘翻了,記Node中通過redis快取session資訊遇到的坑

靠人不如靠己

OK,沒人靠,就靠我的 google 大法了,開始思考,為什麼看別人的例子,別人的 demo 就沒得問題,我就不行,呸,男人不能說自己不行...

what❓ 為什麼我就不 ok❓ 我賊心不死,把官方文件給的 demo 例子又看了一遍,各種操作下來,但是就是拿不到值,我已經蒙圈了 ?

ok,穩住,此路不通,我換條路走,我又去 issues 搜一下,有沒有出現跟我一樣的大哥,發現大哥們好像都沒遇到和我一樣的問題啊,但是還是找到一些可以參考的 issue : Express session object getting removedSessions In API's ... 哭了,我還是沒能看到解決方法,why,why I can't get req.session.email_code

不能慌,穩住,於是去把 Sessions 配置項詳解給看了一遍,嗯,基本知道了每個欄位的含義,讓我們繼續愉快的找 issues 吧,看啊看啊,又看到了兩個 issues,Cookie less version?Cookieless Session,我甚至懷疑是不是我版本問題,於是我就去把 express-session 版本升級了一下,發現並不是,gg,又涼了

然後,突然,想起,好像在 session 配置項裡邊又看到這麼一句話

express-session 在服務端預設會使用 MemoryStore 儲存 Session,這樣在程式重啟時會導致 Session 丟失,且不能多程式環境中傳遞。在生產環境中,應該使用外部儲存,以確保 Session 的永續性。

我們知道,node 是個單執行緒,不像 php 那樣,Node 是一個長期執行的程式,而相反,Apache 會產出多個執行緒(每個請求一個執行緒)

搞這個東西真的是累啊,沒對齊,湊合看吧 ?

                +-----------------+
                |      APACHE     |
                +-+------+------+-+
                  |      |      |
               +--+      |      +--+
      +--------+     +--------+    +--------+
      |   PHP  |     |  PHP   |    |  PHP   |
      | THREAD |     | THREAD |    | THREAD |
      +--------+     +--------+    +--------+
          |              |              |
     +---------+    +---------+    +---------+
     | REQUEST |    | REQUEST |    | REQUEST |
     +---------+    +---------+    +---------+



        +-----------------------------------+
        |                                   |
        |              NODE.JS              |
        |                                   |
        |              PROCESS              |
        |                                   |
        +-----------------------------------+
          |               |              |
     +---------+     +---------+     +---------+
     | REQUEST |     | REQUEST |     | REQUEST |
     +---------+     +---------+     +---------+

複製程式碼

看懂的老鐵雙擊 666,不皮了,我哭了,這次我真的哭了,我換個思路換種做法去做吧,介於 session 沒持久化的玩意,我決定,採用 redis 了. (一開始不用是真的懶...當然也是因為不會...)

從一個坑跳到另一個坑

redis,對我一個前端來說,又是一趟渾水,沒事,百度嘛,反正只要簡單使用就好了,嗯,從安裝到登陸,再到 node 中引用 redisconnet-redis,一頓操作猛如虎,接下來就是真槍實彈了

const session = require('express-session')
const client = require('./config/redis')
const RedisStore = require('connect-redis')(session)

let redisOptions = {
  client: client,
  host: '127.0.0.1',
  port: 6379
}
app.use(
  session({
    secret: 'ticket2019',
    resave: false,
    rolling: true,
    saveUninitialized: true,// 眼熟這個屬性
    cookie: {
      maxAge: 60000,
      secure: true // 眼熟這個屬性
    },
    store: new RedisStore(redisOptions)
  })
)
複製程式碼

老鐵,沒毛病,我看著文件擼的,這時候呢,我們就把 res.session 快取到 redis 中啦,然後呢???然後呢???然後我百度的那些文章就到這裡斷更了,就沒後續了...

ok,我知道它往 redis 存了一個 session 了,於是我去 redis,查一下,是不是真的存了,不要因為我傻,就能欺負我

redis-cli

127.0.0.1:6379> keys *
// sess:Y11FsZ0vgcJFPyJIftuEItLQn8P4rVg-
// sess:MvoJQR8BSQZA6zcfuJFYuJltQH5ZU1rS

127.0.0.1:6379> get sess:Y11FsZ0vgcJFPyJIftuEItLQn8P4rVg-
// {cookie: {}, email_code: '10086'}

127.0.0.1:6379> get sess:MvoJQR8BSQZA6zcfuJFYuJltQH5ZU1rS
// {cookie: {}}

複製程式碼

喲,還真的是存了呀,可是為什麼會有兩個 session???(我真不知道為什麼兩個...),並不是說兩個請求兩個 session,而是我就單單觸發了 retrieveCode() 這個方法進行快取 code,然後redis就兩個session, 你問我為什麼兩個,臣妾真的不知道為什麼啊!!!TMD(暴躁 ing),這又是什麼鬼

魚塘翻了,記Node中通過redis快取session資訊遇到的坑

於是,我就去把 express-session 中的 session 原始碼看了一下,有這麼一段程式碼

if (!req.sessionID) {
  debug('no SID sent, generating session')
  generate()
  next()
  return
}
複製程式碼

然後在 generate() 裡邊做了這個操作

store.generate = function(req) {
  req.sessionID = generateId(req)
  req.session = new Session(req)
  req.session.cookie = new Cookie(cookieOptions)

  if (cookieOptions.secure === 'auto') {
    req.session.cookie.secure = issecure(req, trustProxy)
  }
}
複製程式碼

猜測,是不是每次它都給我生成了一個新的 sessionID,照目前我遇到的情況來看,好像是這樣的,然後繼續去找問題答案,在 issues 看到了這麼一個問題,generating new sessions with an asynchronous store , 嗯,瞭解,繼續找... 然後我發現這麼一個 issue !!!⚠️ 這是一個重大發現!! Cookies disabled results in loss of session (no workaround via Header), 沒錯,翻譯過來就是 : 禁用 cookies 結果就是使得 session 丟失,進去,看看什麼情況

然後看到了這麼一個 comment,是這麼說的:

I have been thinking about this kind of problem recently on my own projects, I know this might not be what you are looking for but it may help others. If you have a login page which users login then send the post request to /login then on success they are sent a cookie and redirected to ie: /bounce and if their session or cookie doesn't exist redirect them to your oh no you don't have cookies enabled if they have a valid session then they are sent to the default home page...

大概意思就是,如果你有一個使用者登入的登入頁面,然後傳送郵件請求 /login, 那麼成功後他們會被髮送一個 cookie 並重定向到 ie /bounce, 如果他們的會話或 cookie 不存在,ok,gg ~

剛講到了 IE 瀏覽器,於是我去寫了個 demo 測試了一下,發現,谷歌瀏覽器好像不能獲取和設定 cookie ?,IE 可以獲取和設定,但是這好像不是重點,於是繼續往下走,這時候就問了一下好友,好像同一個瀏覽器發出的請求會覆蓋 session, 是這樣的嗎?我就沿著這個線出發去尋找答案,然後...然後還是沒能找出個所以然來

我就在這個 issue 裡邊,看別人的回覆和給出的解答,突然想起來,我是不是配置的 session 有問題?禁用 cookies ?禁用 cookies?禁用 cookies?是不是我讓讓 cookie 不隨著傳送,導致的問題?cookie 裡會攜帶一個 sessionID,我通過 sessionID 當作 redis 的 key,key 中存著這個 sessionID 的資訊,穩妥啊

app.use(
  session({
    secret: 'ticket2019',
    resave: false, // 強制session儲存到session store中
    rolling: true, //強制在每一個response中都傳送session識別符號的cookie。如果設定了rolling為true,同時saveUninitialized為true,那麼每一個請求都會傳送沒有初始化的session
    saveUninitialized: false, // 強制沒有“初始化”的session儲存到storage中,如果是要實現登陸的session那麼最好設定為false
    cookie: {
      maxAge: 60000,
      secure: false // 設定為true,需要https的協議
    },
    store: new RedisStore(redisOptions)
  })
)
複製程式碼

我就莫名其妙改啊改啊,就莫名其妙只在 redis 中存一個 session 了,但是極少數情況下還是會存在上一次的 session,這個我真搞不懂了,然後快取了這麼一個email_code,再通過 redis.get(key) 去拿到這個 session,從中取出email_code,應該不是啥大問題了。

然後遇到了非同步的情況,因為我是通過 async / await 的,而 await 是等待一個 promise,所以...並不會按照我意淫安排的那樣,一步一步執行,然後通過 sql 查完之後,再返回資料,而是在我第一次 await 之後,就返回了。。。

/**
 * @desc 獲取token
 * @param {String} email
 */
router.post('/get-token', async (req, res) => {
  try {
    const response = await loginController.retrieveToken(req)
    console.log('???你是不是掉坑了', response) // undefined
    res.json(response)
  } catch (err) {
    throw new Error(err)
  }
})

/**
 * @desc 獲取token
 * @return {Object}
 */
async function retrieveToken(req) {
  const { username, password, email, code } = req.body
  try {
    await redisClient.keys('sess:*', async (error, keyList) => {
      for (let key in keyList) {
        key = keyList[key]
        await redisClient.get(key, async function(err, data) {
          const { email_code } =
            typeof data == 'string' ? JSON.parse(data) : data

          if (code != email_code) {
            // code ...
            // 返回物件告知驗證碼錯誤
          } else {
            try {
              const user = await loginModel.retrieveToken(
                username,
                password,
                email
              )
              return {
                code: types.login.LOGIN_SUCCESS,
                msg: '登陸成功',
                data: {
                  username: user[0].username,
                  token: user[0].token,
                  email: user[0].email
                }
              }
            } catch (error) {
              // code ...
              // 返回物件告知登陸錯誤
            }
          }
        })
      }
    })
  } catch (err) {
    console.info(err)
  }
}
複製程式碼

是的,response 的資料掉坑了,真開心....沒事,這個不是大問題,真的大的問題就是,我到現在腦殼疼,弄了一天,頭腦還是蒙的,遇到不懂的就去查,就去看原始碼看 issue,但是還是沒搞懂,在此,我想問大佬們,你們能給點萌新我一點指導嘛?第一次用 node 擼程式碼,第一次用 redis,都還是第一次...

虛心請教

  • 有沒有適合新手看的又是完成的 demo,參考一下,github 上搜的都太成熟完善了...

  • 上訴有些問題莫名其妙就解決了?比如 2 個 session 我也不知道為什麼改著就成 1 個了...

  • async / await 如何寫才更加好?我感覺自己的程式碼還是很繁雜很亂...

  • ...(有疑問但是不知道如何說...等我想想)

總之,這個功能需求,還沒解決,未待完續...我們江湖見 ✌️


? 3.24 後續更新

對不起,我辜負你了評論區給我指點迷津的大哥們,我還是沒得整對...突然發現後端的同學也挺不容易的~

嗯,我也挺不容易的,腦闊疼了好幾天,我就只想順利畢業 ?,介於畢設太大...(react小程式+vue後臺管理+node後端+mysql的建表+rap2介面文件的書寫),沒得太多時間去琢磨node、redis其中的坑,此時此刻我只想抽自己一巴掌 ?,為什麼自己挖坑給自己跳,別人的畢設都是做個啥xxx考勤系統、二手市場、還有什麼“基於HTML、CSS、JS技術的xxx應用”???

魚塘翻了,記Node中通過redis快取session資訊遇到的坑

你問我為什麼說自己挖坑自己跳?我畢設題目是 : 基於Google V8引擎的分散式訂單系統,牛逼嗎?高大上嗎?管你會不會,題目得先唬到人,導致我這個畢設題目沒人選擇;

然後我炸了,要用node做分散式,然而node第一次用,哭了,還嘴賤,說四月底會把小程式+後臺管理都給做完,老師開心的笑了...而我暢遊在後端的知識海洋中哭了

望各位大哥,能給點做分散式的心得,救救孩子吧 ?

相關文章