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