因为公司的服务端是 C#
, 之前快速开发的应用使用前后端耦合开发;由于目前项目已经前后端分离开发。
这里通过一个基于 Vue全家桶
+ koa2
+MongoDB
完成注册、登录功能并使用token
认证的方式进行权限控制,以便于了解前后端分离的数据交互
基本思路
前端页面构建完成后,通过封装了拦截器的
axios
进行数据请求由koa2
提供的api
接口。 注册完成后登录时,服务端进行数据校验后返回成功并携带token
,前端进行token
的存储;
除了静态数据以及登录注册,其他接口的访问均需要
Header
携带token
值,以便于服务端进行token
校验后返回需要的数据, 进而完成数据交互
前端实现
关于页面的实现数据提交获取以及 Vuex
的使用比较简单,这里不进行介绍;前端部分主要记录路由部分的控制和 axios
拦截的实现
路由控制/拦截
实现方式
-
定义
router
路由配置里面的meta
字段确定该路由需要登录权限; -
通过
vue-router
提供的导航守卫完成我们需要的路由拦截 ,我们这里使用的是全局守卫的beforeEach
。PS : 导航守卫实际是对应的路由行为时触发时执行的钩子函数,我们可以通过
next()
方法阻止/改变此次路由行为; -
路由在完成跳转前都是处于 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
后端部分
后端部分实际上同样并不复杂,关于 mongoose
的model
的定义以及 koa2
与其的交互也是不打算挪列,主要描述 Token
的生成验证以及对应koa2
的配合;
JWT 认证
实现方式
- 客户端登录页输入用户名密码,发送请求给后端 (使用的是明文,这个时候
https
就很重要了,sad~) - 客户端
koa2
获取并解析到内容,密码使用bcrypt
进行校验;如果符合,就下发一个token
返回给客户端。否则返回验证错误信息。 - 登录成功后,客户端将
token
用使用localStorage
,并赋值给vuex
进行存储,之后要请求其他资源的时候,在请求头里带上这个token
进行请求。 - 后端收到请求信息,先验证一下
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
}