魚塘翻了,記Node中經過redis緩存session信息遇到的坑

前戲得作好

大哥們,小弟我又來了,此次真的是請求幫助的了,先說一下,這是第一次用 Node + Express + Mysql 來擼一個項目的後端,正由於第一次,因此遇到了很多的問題,📝 本文除了記錄一下,最後仍是想讓各位大哥給我提供一些方案解決,我...我在這先謝過了javascript

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

  • 用戶登錄,輸入郵箱,點擊 🔘 獲取驗證碼, 發送請求前端

  • 後端經過 nodemailer 給郵箱發送驗證碼vue

  • 發送成功,session 緩存這個codejava

  • 用戶輸入用戶名、密碼、郵箱、驗證碼,進行登錄node

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

  • 相同進行 sql 查詢用戶信息不一樣告知用戶驗證碼不正確react

看似簡單,實際上...😢 真的簡單,可是臣妾真的不會啊。。git

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

搬磚辛酸史

前端代碼就不用說了,就是一個按鈕 🔘,點擊以後發送請求...

/** * @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
複製代碼

大家看看個人,我是後媽生的?爲何個人就是不對呢?

// 存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

  // ...
}
複製代碼

累覺不愛

百思不得其解,而後百度的那些二三十篇文章,臥槽,😠 怎麼都長的如出一轍,千篇一概,底部就都掛着 原文連接友情連接大哥們,大家這樣真的好嗎???

靠人不如靠己

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),這又是什麼鬼

因而,我就去把 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應用」???

你問我爲何說本身挖坑本身跳?我畢設題目是 : 基於Google V8引擎的分佈式訂單系統,牛逼嗎?高大上嗎?管你會不會,題目得先唬到人,致使我這個畢設題目沒人選擇;

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

望各位大哥,能給點作分佈式的心得,救救孩子吧 🙏

相關文章
相關標籤/搜索