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 我司项目示意图