因为公司的服务端是 C#, 之前快速开发的应用使用前后端耦合开发;由于目前项目已经前后端分离开发。
这里通过一个基于 Vue全家桶 + koa2 +MongoDB 完成注册、登录功能并使用token 认证的方式进行权限控制,以便于了解前后端分离的数据交互

基本思路

前端页面构建完成后,通过封装了拦截器的 axios 进行数据请求由 koa2提供的 api 接口。 注册完成后登录时,服务端进行数据校验后返回成功并携带 token,前端进行 token 的存储;

除了静态数据以及登录注册,其他接口的访问均需要 Header 携带 token值,以便于服务端进行 token 校验后返回需要的数据, 进而完成数据交互

前端实现


关于页面的实现数据提交获取以及 Vuex 的使用比较简单,这里不进行介绍;前端部分主要记录路由部分的控制和 axios 拦截的实现

路由控制/拦截

实现方式

  1. 定义 router 路由配置里面的 meta 字段确定该路由需要登录权限

  2. 通过 vue-router 提供的导航守卫完成我们需要的路由拦截 ,我们这里使用的是全局守卫的 beforeEach

    PS : 导航守卫实际是对应的路由行为时触发时执行的钩子函数,我们可以通过 next() 方法阻止/改变此次路由行为;

  3. 路由在完成跳转前都是处于 waiting (等待)的状态,而在一个守卫里: next() 方法的调用便是为了 resolve 当前的钩子,当全部钩子执行完成,此时路由变为 confirmed (确认)状态

import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/user/Login'
import Register from './views/user/Register'
import Manage from './views/user/Manage'
import store from './store'

Vue.use(Router)

// 定义routes 配置
const routes = [
  {
    path: '/login',
    name: 'login',
    component: Login,
    meta: {
      title: '登录'
    }
  },
  {
    path: '/manage',
    name: 'manage',
    component: Manage,
    meta: {
      title: '管理',
      requireAuth: true //定义requireAuth字段确定该路由需要登录权限
    }
  },
  {
    path: '/register',
    name: 'register',
    component: Register,
    meta: {
      title: '注册'
    }
  }
]

const router = new Router({ routes })

//页面进行刷新后,重新赋值 store.user.token
if (localStorage.getItem('token')) {
  store.commit({
    type: 'SET_TOKEN',
    token: localStorage.getItem('token')
  })
}

// 页面跳转权限控制
router.beforeEach((to, from, next) => {
  // 页面需要登录权限
  if (to.meta.requireAuth) {
    if (store.getters.token) {
      next()
    } else {
      // token 无效,重定向到登录页
      Vue.prototype.$message({
        type: 'warning',
        message: '认证过期,需要重新登录'
      })
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    }
  } else {
    next()
  }
})

export default router

Axios 封装

实现方式

  • 使用 axios 的拦截器,统一处理所有http请求和响应;
  • 在需要权限的部分,发起请求时候给 Header 添加 token,处理返回的信息时候,当返回 401未授权时候,信息清除并进行跳转
import axios from 'axios'
import store from './store'
import * as type from './store/constant'
import router from './router'

axios.defaults.baseURL = 'http://localhost:3000/api/'
axios.defaults.headers['Content-Type'] = 'application/json'

// 添加请求拦截器
axios.interceptors.request.use(
  config => {
    // 如果存在token的话,则每个http header都加上
    if (localStorage.getItem('token')) {
      config.headers.Authorization = `Bearer ${store.getters.token}`
    }
    return config
  },
  err => {
    return Promise.reject(err)
  }
)

axios.interceptors.response.use(
  //在后端不直接抛出异常,而是返回对应的状态码,这样我们依旧在response部分进行处理
  response => {
    if (response.data && response.data.status) {
      switch (response.data.status) {
        case 401:
          store.commit(type.REMOVE_TOKEN)
          const currentRouter = router.currentRoute.path
          currentRouter !== 'login' &&
            router.push({
              path: 'login'
            })
          break

        default:
          break
      }
    }
    return response
  },
  err => {
    return new Promise.reject(err.response.data)
  }
)

export default axios

后端部分

后端部分实际上同样并不复杂,关于 mongoosemodel的定义以及 koa2 与其的交互也是不打算挪列,主要描述 Token 的生成验证以及对应koa2的配合;

JWT 认证

实现方式

  1. 客户端登录页输入用户名密码,发送请求给后端 (使用的是明文,这个时候 https 就很重要了,sad~)
  2. 客户端 koa2 获取并解析到内容,密码使用 bcrypt 进行校验;如果符合,就下发一个 token 返回给客户端。否则返回验证错误信息。
  3. 登录成功后,客户端将token用使用localStorage,并赋值给vuex进行存储,之后要请求其他资源的时候,在请求头里带上这个token进行请求。
  4. 后端收到请求信息,先验证一下token是否有效,有效则下发请求的资源否则返回401
  • 使用jsonwebtoken完成token的创建以及编写验证token的中间件
// createToken.js
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('./key')

module.exports = id => {
  // jwt签发token,主体信息和秘钥是必须的
  const token = jwt.sign(
    {
      id,
      exp: Math.floor(Date.now() / 1000) + 60 * 60
    },
    PRIVATE_KEY
  )
  return token
}

// validateToken.js
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('./key')
const User = require('../dbs/userModels').Users

module.exports = async (ctx, next) => {
  const authorization = ctx.get('Authorization')
  if (!authorization) {
    ctx.throw(401)
  }
  // jwt 验证
  jwt.verify(
    authorization.split(' ').pop(), // header Auth为空格分割的字符串
    PRIVATE_KEY,
    async (err, decoded) => {
      if (err) {
        ctx.throw(401) //有一个全局的错误处理中间件,处理后再发给前端
      } else {
        // 利用ctx作为数据传递,为了后续的koa2 中间件获取到_id
        ctx.id = decoded.id
      }
    }
  )
  await next()
}
  • koa2 提供用户的注册和登录逻辑API,如果需要使用jwt的验证,只需要在处理函数之前使用validateToken中间件进行处理即可
const Router = require('koa-router')
const User = require('../dbs/userModels').Users
const createToken = require('../token/createToken')
const validateToken = require('../token/validateToken')
const router = new Router()

// 用户注册逻辑
router.post('/api/register', async ctx => {
  const { username, password } = ctx.request.body
  const isRegister = await User.find({ username })
  if (isRegister.length > 0) {
    ctx.body = {
      code: -1,
      message: '该用户名已被注册'
    }
    return
  }
  const user = await User.create({
    username,
    password
  })
  if (user) {
    ctx.body = {
      message: '用户注册成功',
      code: 0
    }
  } else {
    ctx.body = {
      message: '用户注册失败,请重试',
      code: -1
    }
  }
})

// 用户登录逻辑
router.post('/api/login', async ctx => {
  const { username, password } = ctx.request.body
  // 验证用户是否存在
  const user = await User.findOne({ username })
  if (!user) {
    return (ctx.body = {
      message: '用户名不存在',
      code: -1
    })
  }
  // 验证密码 && 生成token并下发
  const isPasswordValid = require('bcrypt').compareSync(password, user.password)
  if (isPasswordValid) {
    const token = createToken(user._id)
    ctx.body = {
      token: token,
      message: '登录成功',
      code: 0
    }
  }
})

module.exports = {
  router
}