1.背景:
項目前后台分離, 前端技術棧Nuxt.js + express.js 三台服務器 后端 5台服務器 做負載均衡處理
2.問題:
后端不做用戶狀態緩存, 僅通過user_id + user_acc 等 做AES加密生成 token,請求響應解密token 是否正確,
前端token如果只是本地緩存或狀態層做token的非空驗證,無法鑒別token是否偽造
3.解決思路:
在node中間層做用戶token鑒權;
4.解決方法:
4.1 寫入狀態: 用戶登錄 --> node中間層標記cookie(例: nd-token: tokenKey(uuid + timestamp等生成唯一string)) --> 后端生成 user-token 返回node層 --> node層寫入redis (key為 tokenKey, value 為后端返回 user-token )
4.2 (中間層)鑒權: 用戶交互 --> node中間層讀取cookie : nd-token;
4.2.1 若 nd-token 為空, 判斷為游客狀態;
4.2.2 若 nd-token 存在, 獲取 tokenKey , 讀取 redis中, tokenKey的value: user-token --> 攜帶token 請求后端 ( 這里可以根據前后約定, user-token 放header 或 body )
5. 后續處理:
a. 4.1 步驟中: 寫入 redis 的時候, expire; (redis過期銷毀, 對應cookie 可以設置也可以不設置, 若cookie獲取到, 查詢redis 查詢不到 即登錄狀態過期)
b. 退出登錄狀態: 用戶退出登錄 --> node中間層 讀取cookie : nd-token , redis 刪除對應 tokenKey; --> 后端退出登錄 --> 返回退出成功
6. 代碼部分
6.1 業務代碼:
redis.js // 這里我司是部署阿里雲內網,全是走內部通信
const redis = require('redis') // const host = '172.XXX.XXX.182' // 內網 const host = '47.XXX.XXX.165' // 外網 // const host = '127.0.0.1' const port = 6379 const redisClient = redis.createClient({ host, port, db: 3 }) redisClient.on('error', err => { console.log(err) }) module.exports = redisClient
server/index.js
const session = require('express-session') const redisClient = require('../redis') const RedisStore = require('connect-redis')(session) const sessionStore = new RedisStore({ client: redisClient }) // session配置 app.use( session({ secret: 'super-secret-key', cookie: { maxAge: 60 * 1000 }, resave: false, saveUninitialized: false, rolling: true, store: sessionStore // 存儲在redis中 }) )
// 登錄重寫 app.post('/re-api/login', function(req, res) { const cookie = req.headers.cookie request( { headers: { cookie, Accept: 'application/json, text/plain, */*' }, url: env.API_URL + '/user/user-login', method: 'post', json: true, body: req.body }, (err, response, data) => { if (err) { return res.json({ errorCode: 500, errorMsg: err }) } const signature = sign(new Date().getTime()) if (data.code === 200) { const token = data.data.user_token // token 寫入 redis redisClient.set(signature, token, err => { if(err) { console.log('set redis error', err) } else { console.log('set redis success') // 設置過期時間 1小時 redisClient.expire(signature, 60 * 60) } }) // 記錄 cookie res.cookie('nd-token', signature, { maxAge: 900000 }) } return res.json(data) } ) })
serverMiddle/index.js // nuxt.js 提供 serverSide 的中間件入口 在 nuxt.config.js 中配置
const redisClient = require('../redis') export default function (req, res, next) { const sign = req.cookies['nd-token'] || '' if (sign) { // 獲取當前訪問的 cookie 獲取對應redis 鍵值 redisClient.get(sign, function (err, hmgeted) { if (err) { console.log('get redis error ', err) } else { console.log('get redis success!') // 寫入 session層 req.session.userToken = hmgeted // 更新過期時間 redisClient.expire(sign , 60 * 60) } next() }) res.cookie('nd-token', sign , { maxAge: 900000 }) } else { next() } }
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import app from './modules/app' import user from './modules/user' import getters from './getters' Vue.use(Vuex) const createStore = () => { return new Vuex.Store({ modules: { app, user }, actions: { nuxtServerInit({ commit }, { req, res }) { if (req.session.userToken) { commit('SET_TOKEN', req.session.userToken) } else { console.log('nuxtserverinit token no find') } } }, getters }) } export default createStore
6.2 服務端代碼(nginx負載均衡)
主服務器 A:
vhost/nodeServerA.conf
server { listen 20010; server_name 'nodeServerA.cn'; location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://127.0.0.1:20011; } } server { listen 20009; server_name 'nodeServer.cn'; location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://backend; } }
nginx.conf
upstream backend { server nodeServerA.cn:20010; server nodeServerB.cn:20010; server nodeServerC.cn:20010; }
服務器B
server { listen 20010; server_name 'nodeServerB.cn'; location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://127.0.0.1:20011; } }
服務器C
server { listen 20010; server_name 'nodeServerC.cn'; location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://127.0.0.1:20011; } }
7.說明
7.1 Nuxt.js 官網 提供的案例是直接 寫入 session 緩存做 store的狀態寫入處理, 這里的問題是 單線程 和 多台服務器均衡負載;
7.2 我司項目示意圖