nuxt+koa-session2+redis 实现用户登录

前言

最近再用nuxt2+elementui编写用户登录和注销,在实际操作过程中发现了许多问题,目前关于登录的会话保存除了用token就是用session,我依然采用传统的session方式,同时由于我nuxt的服务端使用的是koa2,所以打算采用koa的session相关插件来实现此功能,这时市面上主流的就两种,一种是koa-session,一种是koa-session2。首先我们需要理清二者的区别,从而选择使用哪一个。
koa-session和koa-session2在不采用外部存储的时候,koa-session会直接将要保存的数据存入客户端cookie中,koa-session2则不同,它是将要保存的数据存入内存中,(注意:koa-session和koa-session2都会保存cookie到客户端,该cookie类似于sessionid用于去找保存的内容,对于要保存的数据内容,存储的地方不一样)从这一点看,koa-session单纯的存入客户端cookie中,不利于数据的安全性,如果保存的数据中有密码之类,则更加不安全,相比koa-session2,它将数据存入服务端内存中,安全性较高,但相对而言,但是只要服务器重启,内存中保存的session都会释放,所以即使客户端的cookie未失效,也找不到服务端的相关信息。这两者的优缺点明确之后,我们才可以选择对应的模块,我这里选择使用koa-session2,ok,既然决定了使用方法,我们就来看下怎么使用。
我这里不光使用了koa-session2,同时使用了redis作为session数据的外部保存,不再将数据默认存储到内存,而是保存到redis,正因为使用了redis,所以使用了ioredis模块来连接本地的redis服务

正文

安装koa-session2

npm install --save koa-session2
npm install --save ioredis

然后在nuxt项目中的server/index.js中使用

// const Koa = require('koa')
import Koa from 'koa'
import session from 'koa-session2'
import Store from './util/redisStore'
import users from './interface/users'
import posts from './interface/posts'
const bodyParser = require('koa-bodyparser')

const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')

const app = new Koa()

//设置配置session的加密字符串,可以任意字符串
app.keys = ['some secret hurr']

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
// const redisStore = require('./util/redisStore')
config.dev = app.env !== 'production'

// 获取数据连接和初始化方法
const { connect, initSchema } = require('./dbs/init')

async function start() {
  // Instantiate nuxt.js
  const nuxt = new Nuxt(config)

  const {
    host = process.env.HOST || '127.0.0.1',
    port = process.env.PORT || 3000
  } = nuxt.options.server

  // Build in development
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // 立即执行函数,连接数据库
  ;(async () => {
    await connect()
    initSchema()
  })()

  // 配置session
  app.use(
    session({
      store: new Store()
    })
  )

  // 配置解析post的bodypaser
  app.use(bodyParser())
  // 配置服务端路由
  app.use(users.routes()).use(users.allowedMethods())
  app.use(posts.routes()).use(posts.allowedMethods())

  app.use((ctx) => {
    ctx.status = 200
    ctx.respond = false // Bypass Koa's built-in response handling
    // ctx.req.session = ctx.session
    ctx.req.ctx = ctx // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash
    nuxt.render(ctx.req, ctx.res)
  })

  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}

start()

这里的./util/redisStore.js文件的内容:

import Redis from 'ioredis'
import { Store } from 'koa-session2'

export default class RedisStore extends Store {
  constructor() {
    super()
    this.redis = new Redis()
  }
//根据sessionid从redis取数据
  async get(sid) {
    const data = await this.redis.get(`SESSION:${sid}`)
    return JSON.parse(data)
  }
//根据sessionid往redis中放数据
  async set(session, opts) {
    if (!opts.sid) {
      opts.sid = this.getID(24)
    }
    console.log(`SESSION:${opts.sid}`)
    await this.redis.set(`SESSION:${opts.sid}`, JSON.stringify(session))
    return opts.sid
  }
//根据sessionid从redis删除数据
  async destroy(sid) {
    await this.redis.del(`SESSION:${sid}`)
  }
}

./util/redisStore.js文件的内容不是我自己写的,参考了官网的写法,网上太多乱七八糟的写法了,还是官网靠谱。
当然这里我遇到一个问题,那就是使用原版的koa-session2会出错,出错的原因在于我使用了babel-node,不是原始的node,因而,我是用的koa-session2是特殊的koa-session2@babel版的,
所以我这里重新安装了koa-session2@babel

npm install --save koa-session2@babel

同时参考了官网的写法,一定要参考官网,上面的store的配置文件才妥妥的。
到此,所有的配置都ok了,接着我们来看登录的服务端的内容,我是将登录的内容写在/server/interface/users.js中

const Router = require('koa-router')
// const axios = require('axios')
const mongoose = require('mongoose')
const router = new Router({ prefix: '/users' })

// 管理员登录
router.post('/signin', async (ctx, next) => {
  console.log('1.----signin')
  const UsersModel = mongoose.model('users')
//从前端获取登录的表单信息
  const loginInfo = ctx.request.body
//利用mongoose从数据库中查询用户信息,排除密码不查
  const result = await UsersModel.findOne(
    {
      username: loginInfo.username,
      password: loginInfo.password,
      type: 'administrator'
    },
    {
      _id: 1,
      username: 1,
      tel: 1,
      email: 1,
      createAt: 1,
      lastLoginAt: 1,
      type: 1
    }
  ).then((res) => {
    if (res) {
      // 将数据库中查询出的用户信息存入session中
      console.log('userinfo***********res', res)
      ctx.session.user = res
      return {
        result: 'success',
        user: res
      }
    } else {
      return {
        result: 'failed'
      }
    }
  })
  ctx.body = result
})

// 管理员登出
router.get('/signout', (ctx, next) => {
  console.log('2.----signout')
  ctx.session = null
  ctx.body = {
    result: 'success'
  }
})
export default router

接着我们来看下前端登录部分的实现

<template>
  <div class="login-wrap">
    <div class="ms-login">
      <div class="ms-title">后台管理系统</div>
      <el-form
        ref="loginForm"
        :model="param"
        :rules="rules"
        label-width="0px"
        class="ms-content"
      >
        <el-form-item prop="username">
          <el-input v-model="param.username" placeholder="username">
            <el-button slot="prepend" icon="el-icon-custom-user"></el-button>
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="param.password"
            type="password"
            placeholder="password"
            @keyup.enter.native="submitForm()"
          >
            <el-button
              slot="prepend"
              icon="el-icon-custom-password"
            ></el-button>
          </el-input>
        </el-form-item>
        <div class="login-btn">
          <el-button type="primary" @click="submitForm()">登录</el-button>
        </div>
        <p class="login-tips">Tips : 用户名和密码暂时可以随便填。</p>
      </el-form>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
export default {
  data() {
    return {
      param: {
        username: 'admin',
        password: '123123'
      },
      rules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 3, message: '长度在 3 个字符以上', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    ...mapMutations(['set_user']),
    submitForm() {
      this.$refs.loginForm.validate((valid) => {
        if (valid) {
          this.$axios
            .post('/users/signin', this.param, { withCredentials: true })
            .then((res) => {
              if (res.data.result === 'success') {
                this.$message.success('登录成功')
                //利用vuex的mutation,将登录用户的数据存入vuex中的state中
                this.set_user(res.data.user)
                this.$router.push('/dashboard')
              } else {
                this.$message.warning('登录失败')
              }
            })
        } else {
          this.$message.error('请重新输入账号和密码')
          return false
        }
      })
    }
  }
}
</script>

<style scoped>
.login-wrap {
  position: relative;
  width: 100vw;
  height: 100vh;
  background-image: url('~assets/img/login-bg.jpg');
  background-size: 100%;
}
.ms-title {
  width: 100%;
  line-height: 50px;
  text-align: center;
  font-size: 20px;
  color: #666;
  border-bottom: 1px solid #ddd;
}
.ms-login {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 350px;
  margin: -190px 0 0 -175px;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.3);
  overflow: hidden;
}
.ms-content {
  padding: 30px 30px;
}
.login-btn {
  text-align: center;
}
.login-btn button {
  width: 100%;
  height: 36px;
  margin-bottom: 10px;
}
.login-tips {
  font-size: 12px;
  line-height: 30px;
  color: #fff;
}
</style>

前端核心的点就是将登录后的,从服务端传回的用户数据存入到vuex的state中,就是this.set_user(res.data.user)这句话,但是使用vuex会有一个bug,那就是在页面刷新的时候,vuex中的state的内容会丢失,所以我们在传统的vue项目中,会再采用localstorage去保存,这样在刷新之后,state可以利用localstorage来还原,但是因为我们使用的基于ssr的nuxt项目,nuxt提供了一个方法沟通前后端,那就是nuxtServerInit,该方法在/store/index.js中

export const state = () => ({
  authUser: null
})

export const mutations = {
  set_user(state, user) {
    state.authUser = user
  }
}
export const actions = {
  // 该方法用于解决当页面刷新时,vuex内容丢失,
//同时由于每次刷新都会调用nuxtServerInit方法,
//这时,我们可以将session取出来再次放入state中
  nuxtServerInit({ commit }, { req, app }) {
    // 将session中的用户存储到vuex的state中
    console.log('**********nuxtserverInit')
    if (req.ctx.session.user) {
      console.log('store---', req.ctx.session.user)
      commit('set_user', req.ctx.session.user)
    }
  }
}

注意:req.ctx.session.user,因为session是放在koa的ctx中的,nuxt2考虑到了这点,所以将ctx放入到了req中,参考server/index.js中的这段:

app.use((ctx) => {
    ctx.status = 200
    ctx.respond = false // Bypass Koa's built-in response handling
    ctx.req.ctx = ctx // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash
    nuxt.render(ctx.req, ctx.res)
  })

最后

最后我们还需要解决一个问题,就可以开心的使用了,那就是权限认证,当我访问其他页面的时候,如果是未登录用户,那么会自动跳转到login.vue去显示,所以这时我们需要编写一个nuxt的中间件(middleware):/middleware/auth.js

export default function({ app, req, redirect, route, store }) {
  // 该中间件用于判断state中用户是否存在,如果不存在,则跳回登录页面
  console.log('middleware--auth')
  console.log('************req', req.ctx.session)
  //虽然这里在刷新后,能取到req.ctx.session,但是单纯的router.push类的客户端跳转
  //是不会走服务端的,所以是拿不到req.ctx.session的,所以只能使用state来判断
  if (!store.state.authUser) {
    redirect('/login')
  }
}

编写完的中间件auth.js,我们将它放在指定的路由上,当刷新访问这个路由的时候,会经过该中间件进行判断,我这里是将中间件放在/pages/index.vue中,middleware: 'auth',

<template>
  <div class="wrapper">
    <v-head :auth-user="authUser"></v-head>
    <v-sidebar></v-sidebar>
    <div class="content-box">
      <div class="content">
        <transition name="move" mode="out-in">
          <nuxt-child />
        </transition>
        <el-backtop target=".content"></el-backtop>
      </div>
    </div>
  </div>
</template>

<script>
import vHead from '~/components/Header.vue'
import vSidebar from '~/components/Sidebar.vue'
export default {
  middleware: 'auth',
  components: {
    vHead,
    vSidebar
  },
  data() {
    return {
      authUser: null
    }
  },
  asyncData({ app, store }) {
    console.log('dashboard----', store.state.authUser)
//这里将state中的authUser传给v-head组件,v-head组件去显示登录用户信息
    return {
      authUser: store.state.authUser
    }
  }
}
</script>
<style lang="scss" scoped></style>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容

  • koa-session是koa的session管理中间件,最近在写登录注册模块的时候学习了一下这部分的代码,感觉还...
    fankaife阅读 22,117评论 4 26
  • 2017年跟着教程做了一个全栈的商场(vue + express + mongodb),2019年,工作中一直做前...
    FinGet阅读 550评论 0 2
  • Lesson-1 Session Session是什么 其实就是用户的认证与授权。认证与授权又是什么?认证,就是让...
    羽晞yose阅读 1,147评论 0 1
  • 注:本文转载自前端大全 背景 在HTTP协议的定义中,采用了一种机制来记录客户端和服务器端交互的信息,这种机制被称...
    楠小忎阅读 663评论 0 0
  • 你从不说你爱我。 没有什么别的了。 我沉默无语。 父亲,有的沉默是绳索。 束缚了自我,却提供了保护。 正如我和你的...
    津岛修之芥阅读 166评论 0 0