您现在的位置是:首页 >技术交流 >vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录网站首页技术交流

vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录

泰瑞宝的样 2024-05-09 00:00:02
简介vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录

前言

  • 采用vue3,vue-router版本为4.x
  • 前端构建工具采用vite
  • IDE采用VSCODE,安装了MYSQL客户端插件

前端编写

安装并使用 vue-router

如果有vue-router,就略过这一小节。
vue-router完整教程:点这里>>

第一步:npm安装

npm install vue-router@4

第二步:新建路由文件 router/index.ts 用来配置路由,新建pages目录用于存放页面文件

在这里插入图片描述

第三步:在pages目录中添加登录页(login)和首页(index),以备路由使用

在这里插入图片描述
两个页面的内容先简单写成显示“index”和"login",以index页为例:

<script setup lang="ts">
import { ref } from "vue";
const t = ref("index");
</script>
<template>
  <div>{{ t }}</div>
</template>

第四步:编写路由配置文件

在 router/idnex.ts 文件中编写如下代码

import {
    createRouter,
    createWebHistory,
    RouteRecordRaw,
    createWebHashHistory,
} from "vue-router";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/",
        redirect:"/index",
        /** 路由重定向:当地址路径为 / 时,将地址路径重定向为 /index */
    },
    {
        path: "/login",//路由路径
        name: "Login",//路由名称(暂且当做路由的ID或KEY)
        component: () => import("../pages/login/login.vue"),//路由页面
        /** 这段路由配置的意思就是:当地址路径为 /login 时,页面将显示../pages/login/login.vue的内容 */
    },
    {
        path: "/index",
        name: "Index",
        component: () => import("@/pages/index.vue"),//@是alias配置的别名,表示src/,若要使用请配置
    },
]

const router = createRouter({
    history: createWebHashHistory(),
    routes,
});
export default router;

(若要使用alias别名,请参阅《VUE+ts项目配置–alias别名配置》

第五步:使VUE应用路由

在main.ts 文件中,编写如下代码:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'//引入配置的路由

createApp(App).use(router).mount('#app')//应用该路由

第六步:放置路由出口

路由出口即路由显示的位置。
我把路由出口放在App.vue文件中:

<script setup lang="ts">
</script>
<template>
  <router-view></router-view><!--路由出口-->
</template>
<style scoped></style>

这样一个简易的前端路由就搭建好了。开启vue服务环境npm run dev即可在浏览器看到不同的路由页面。
在这里插入图片描述

安装Less

Less可以更方便的编写CSS样式。如果只想使用CSS或你已安装了SCSS或其他同类型的插件可以跳过此节。
安装方法可参考《Vue 3中引入SCSS和LESS依赖的教程指南》

我这里只

npm install less

就可以使用了。

整合axios插件

《vue+Nodejs+Koa搭建前后端系统(一)–简易版》中已经安装并使用了axios,但每次使用都得写好多代码。我们需要整合一下它。

第一步:在 src/ 目录下新建http.ts文件
在这里插入图片描述
第二部:在http.ts中编写axios的基础配置

我这里修改了

  1. axios的默认配置
  2. 改写了原来的get方法,使其可以像post方法一样传 json 形式的参数
  3. 使用请求和响应拦截(请求拦截暂时没用到,响应拦截用来监测每次http请求是否有正确的登录信息)
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
import qs from "qs";/** npm install qs */
import { ElMessage } from "element-plus";
import router from '@/router/index'

/** 改写axios get方法时的接口 */
interface MyAxiosInstance extends AxiosInstance {
    get<T = any, R = AxiosResponse<T>, D = any>(url: string, params?: { [propName: string]: any }, config?: AxiosRequestConfig<D>): Promise<R>;
}

/** 创建axios实例,修改默认配置 */
const http:MyAxiosInstance = axios.create({
	/**默认请求根路径:判断是开发环境和是生产环境 */
	/** 
	  开发环境production:/nodeApi/ 我的vite.config.ts中设置的代理是/nodeApi 
	  生产环境development:http://localhost:5152/ 
	*/
    baseURL: process.env.NODE_ENV === 'production' ? 'http://localhost:5152/' : '/nodeApi/',
    timeout: 60000,/** 请求超时时间设为1分钟 */
});
/**改写原来的axios.get(url,[config])为axios.get(url,[params,[config]]) */
http.get = (url: string, params: any = {}, config?: any) => {
    let url_search = qs.stringify(params);//序列化params
    if (url_search) {
        url_search = "?" + url_search;
        url = url + url_search;
    }
    return http.request({
        method: 'get',
        url: url,
        ...config
    })
}
/**请求拦截器(在请求发送之前触发):暂时什么也没做*/
http.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么,config是请求的一些配置
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
/**响应拦截器(在服务器响应后第一时间触发) */
http.interceptors.response.use(function (response) {
    // 对响应数据做点什么,response是响应的数据
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    // 这里假定在服务器验证登录身份失败后会抛出错误,并将状态码改为401
    if (error?.response.status === 401) {
        router.push("/login");//路由到登录页
    }
    return Promise.reject(error?.response.data);//继续向后传递错误
});
export default http //导出axios实例,以供其他页面使用

这里需要注意的是:在非vue文件(比如ts文件)中要使用vue-router,请引用路由配置文件,像这样

import router from '@/router/index'
router.push("/login");

而不是

import {useRouter} from 'vue-router'
const router = useRouter();//这里得到的router是undefined
router.push("/login");

漂亮的登录页面

小树苗已经破土,接下来我们给他开枝散叶吧!

登录页面路径 pages/login/login.vue , 页面样式随心情写,只要一点:要有登录功能。
为了减少编写代码量和页面的美观,我决定安装一下组件库Element Plus

npm install element-plus --save

main.ts文件:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'//引入ElementPlus组件
import 'element-plus/dist/index.css'//引入ElementPlus样式

createApp(App).use(router).use(ElementPlus).mount('#app')//使用ElementPlus

login.vue文件:

<script setup lang="ts">
import { reactive, ref } from "vue";
import type { FormRules, FormInstance } from "element-plus";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";
import http from "@/http.ts";//引入axios实例

const router = useRouter();
const ruleFormRef = ref<FormInstance>();//Elementu Plus表单组件ref,可以看成是组件实例
/** 登录传递后台的参数:正常情况下密码password需要加密处理的,这里只做演示用,未加密 */
const formData = reactive({
  username: "",
  password: "",
});
/** Element Plus表单验证 */
const rules = reactive<FormRules>({
  username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
  password: [{ required: true, trigger: "blur", message: "请输入密码" }],
});
/** 登录 */
const submit = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {//表单验证成功,则请求后端登录接口
      http.post("login/loginIn", formData).then((data: any) => {
      	/** 登录成功:假定登录成功,返回的http状态码为200,信息在message字段 */
        ElMessage({
          message: data.message,
           type: "success",
         });
         router.push("/index");//路由到首页
      }).catch((err: any) => {
      	  //登录失败:后台抛出异常,前端提示错误信息(包括登录失效),错误信息在message字段
          ElMessage({
            message: err.message,
            type: "error",
          });
       });
    } else {//表单验证失败
      ElMessage({
        message: "请按提示登录",
        type: "error",
      });
    }
  });
};
</script>
<template>
  <div class="login">
    <div class="login-card">
      <div class="title">阳光海滩欢迎您</div>
      <el-form ref="ruleFormRef" :model="formData" status-icon :rules="rules">
        <el-form-item label="用户" prop="username">
          <el-input v-model="formData.username" />
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="formData.password"
            type="password"
            autocomplete="off"
          />
        </el-form-item>
        <el-form-item>
          <el-button
            class="login-btn"
            type="primary"
            size="large"
            @click="submit(ruleFormRef)"
            >登录</el-button
          >
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<style lang="less" scoped>
.login {
  width: 100vw;
  height: 100vh;
  box-sizing: border-box;
  overflow: hidden;
  background-image: url(../../assets/Images/login.jpg);/一张登录背景图
  background-repeat: no-repeat;
  background-size: cover;

  .login-card {
    display: inline-block;
    margin: auto;
    margin-top: 50vh;
    transform: translateY(-50%);
    padding: 30px 60px;
    background: rgba(255, 255, 255, 0.5);
  }
  .title {
    font-size: small;
    margin-bottom: 15px;
  }
  .login-btn {
    display: block;
    width: 100%;
  }
}
</style>

最终效果图:

在这里插入图片描述

首页(/pages/index.vue)编写

为了接下来验证登录失效是否会跳回登录页,我们可以在首页写一个获取所有户信息的表格:

<script setup lang="ts">
import { ref } from "vue";
import http from "@/http.ts";
import { ElMessage } from "element-plus";

const isload = ref(false);//加载图标是否显示
const list = ref([]);

const lookUser = async () => {
  const params = {};
  isload.value = true;
  await http.post("users/look", params)
   	   .then((data: any) => (list.value = data.list))
       .catch((err: any) => {
          //后台抛出异常,前端提示错误信息(包括登录失效),错误信息在err.message中
	      ElMessage({
	        message: err.message,
	        type: "error",
	     });
       });
  isload.value = false;
};
lookUser();
</script>
<template>
  <div class="index">
    <el-table :data="list" style="width: 100%" v-loading="isload">
      <el-table-column prop="username" label="用户名" />
      <el-table-column prop="password" label="密码" />
      <el-table-column prop="create_time" label="创建时间" />
    </el-table>
    <el-button class="refresh-btn" @click="lookUser">刷新列表</el-button>
  </div>
</template>
<style lang="less" scoped>
.index {
  width: 100%;
  .refresh-btn {
    margin-top: 20px;
  }
}
</style>

最终效果图:
在这里插入图片描述

后端编写

后端的接口编写要保证每个与用户信息相关的接口进行用户信息验证。可以采用的验证方式有两种:cookie和token令牌。

这两种方式各有优缺点:

  • cookie方式如其名,他必须依赖支持cookie的浏览器,对于app、部分手机端就无能为力,且不可以跨域。其优点是浏览器的cookie会在每次请求自动发送给服务器,无需前端特意编写代码。
  • token令牌能够弥补cookie的缺点,但一旦token生效,直到其过期,这期间一直有效,无法将其手动改变为失效(除非重启服务器),这就会导致一旦token被窃取是十分危险的,你只能眼睁睁看着窃贼犯罪。

cookie验证

第一步:安装 koa-session

npm install koa-session

koa-session即是整合服务端会话和客户端cookie的插件。

第二步:新建一个中间件文件 /middleware/session.js

在这里插入图片描述

第三步:编写session中间件

/middleware/session.js

const session = require('koa-session');

const sessionCtxKey = "userInfo";//用户信息存储在ctx.session对象中的key键名

/** 添加session中间件方法 */
/** 参数app是Koa实例 */
function takeSession(app) {
	/** session配置 */
    const CONFIG = {
        key: 'koa.sess',//cookie 中 sessionId 的格式
        maxAge: 180000,//session 最大存活周期, 单位 ms
        autoCommit: true,//是否自动将 session 及 sessionid 提交至 header 返回给客户端
        overwrite: true,//是否可覆盖
        httpOnly: true,//客户端是否可访问
        signed: true,//是否应用签名
        rolling: true,//是否每次响应刷新 session 有效期
        renew: false,//是否在会话即将过期时更新会话
    };
    app.keys = ['xiaoyang'];//用于加密 cookie, signed  为 true 时必填 ! 数组中如果多于一个项, 则会用于密钥轮换。
    return session(CONFIG, app);
}

/** session验证用户信息方法 */
/** 参数p是一个对象,其中no_verify字段用来配置不需要验证的路由 */
function verifySession(p = {}) {
    const defaultP = {
        no_verify: [],
    };
    const currentP = {};
    Object.assign(currentP, defaultP, p);
    return async function (ctx, next) {
    	const requestUrl = ctx.request.url.replace(/?.*$/gim, "");
        if (currentP.no_verify.includes(requestUrl)) {//不需要验证token的接口(GET请求去掉?及后面的参数再进行比较)
            await next();
        } else {//验证用户信息
            if (ctx.session[sessionCtxKey]) {//验证通过
                await next();
            } else {//验证未通过
                ctx.body = { message: "登录状态失效,请重新登录!", code: -1 };
            }
        }
    }
}
module.exports = {
    takeSession,
    verifySession,
    sessionCtxKey
}

第四步:使用session中间件

app.js

const Koa = require("koa");
const app = new Koa();
const { takeSession, verifySession } = require("./middleware/session.js")

/**添加session和验证session中间件 START*/
app.use(takeSession(app));
app.use(verifySession({ no_verify: ["/login/loginIn"] }));//登录接口不验证session
/**添加session和验证session中间件 END*/

/**
	这里简要写的,app.js中的其他代码请看前面章节
	!!!需要注意的是session中间件要写在路由中间件的前面,否则session不会起作用!!!
*/

第五步:新建 /module/login.js文件

在这里插入图片描述

第六步:编写用户登录处理接口

/module/login.js

//用户登录
async function loginUser(ctx, next) {
    const { sessionCtxKey } = require("../middleware/session")//引用用户信息存储在ctx.session对象中的key键名
    const params = ctx.request.body;
    const username = params.username;//入参:用户名
    const password = params.password;//入参:密码
    const sql = `SELECT id, password FROM create_user WHERE username='${username}'`;

    try {
        const r = await ctx.db.query(sql);
        let result;
        if (r && (result = r[0])) {//查到该用户
        	//该用户的密码与客户端输入的是否一致
            if (result.password === password) {
                ctx.session[sessionCtxKey] = { username: username, userID: result.id };
                ctx.body = { message: '登录成功', id: result.id, code: 0 }
            } else {
                ctx.response.status = 403;
                ctx.body = { message: "密码错误", code: 1 };
            }

        } else {//未查到该用户
            ctx.response.status = 403;
            ctx.body = { message: "未查到该用户", code: 2 };
        }
    } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
    }
}

module.exports = {
    loginUser
};

第七步:登录路由处理

登录的路由文件在 /routes/login.js 中。在《vue+Nodejs+Koa搭建前后端系统(五)–Nodejs中使用数据库》中,我已将路由的书写形式改成我喜欢的样子,这里不再赘述,建议看看。

/routes/login.js

const { loginUser } = require("../module/login")
module.exports = [
  {
    url: "/loginIn",
    methods: "post",
    actions: loginUser
  },
];

第八步:修改 users/look 路由接口

修改 /module/login.js 中的 lookUser 方法

/***/
//查看所有用户
async function lookUser(ctx, next) {
  const sql = `SELECT * FROM create_user`;
  try {
    ctx.body = {
      message:"查询成功",
      list: await ctx.db.query(sql),
    }
  } catch (e) {
    ctx.response.status = 500;
    ctx.body = { message: e, code: 99 };
  }
}
module.exports = {
  lookUser
};

这样一个依赖cookie的登录就写好了。

下面说说我对koa-session的理解:
app.use(takeSession(app));其实就是官方给的范例中的app.use(session(CONFIG, app));

在这里插入图片描述
其作用是在应用中加载该插件:

  1. 在context上下文中寻找session字段(对象),没有则添加该字段
  2. 在服务器中寻找session开辟的内存,没有则开辟之。该内存中存储已登录用户的信息(我这里是username、userID)、用户信息存储在ctx.session对象中的key键名和cookie信息等,并定时清理过期的用户信息,即为用户信息维护表。这里用sessionId作为用户信息的key,以备后续查找
  3. 每次请求会获取客户端发送过来的cookies信息(sessionId),则查找用户信息维护表中key为该sessionId的用户信息和cookie信息(对用户信息维护表的维护按照CONFIG执行),并将用户信息写入context上下文的session对象中,该对象的key为用户信息维护表中
  4. 执行登录接口时,向context上下文的session对象赋值,比如ctx.session.userInfo = {username:'xiaoyang',userID:1},则会生成sessionId,并将该值写入用户信息维护表中,在服务器响应时,该sessionId会被当做cookie发送给客户端。

内存中的用户信息表类似于这样:

app.context.sessionTable = {
	'sessionId_1':{username:'xiaoyang',userID:1,expires:'2023-6-15',domain:'127.0.0.1',path:'/',ctxSessionKey:"userInfo"},
	'sessionId_2':{username:'xiaoyang1',userID:5,expires:'2023-6-15',domain:'127.0.0.1',path:'/',ctxSessionKey:"userInfo"}
}

接下来是app.use(verifySession({ no_verify: ["/login/loginIn"] })),它就是用来验证用户信息是否存在并有效。核心点就是查看context上下文的session对象相应的key上是否被赋值了。

token验证

第一步:安装 jsonwebtoken

npm i jsonwebtoken

第二步:新建一个中间件文件 /middleware/jwt.js

第三步:编写jsonwebtoken中间件

/middleware/jwt.js

const jwt = require("jsonwebtoken");
const secretkey = "xiaoyang";//秘钥
const CONFIG = { expiresIn: 60 };//jwt配置:60s后过期
//服务器每隔30s刷新一次token(之前未过期的token依然好用)
//!!!REFRESH_TIMES 不能大于CONFIG.expiresIn !!!
const REFRESH_TIMES = 30;
let create_at = 0;//最新的token刷新时间
/** 刷新token */
function takeToken(Payload = {}) {
  create_at = new Date().getTime();
  const salt = Math.random();//加盐:使token更不易被效仿、伪造
  return jwt.sign({ create_at: create_at, salt: salt, ...Payload }, secretkey, CONFIG);
}
/** 验证token是否有效(中间件用) */
function verifyToken(p = {}) {
  const defaultP = {
    no_verify: [],//不需要验证token的接口
  };
  currentP = {};
  Object.assign(currentP, defaultP, p);
  let newToken = takeToken();//最新的token
  setInterval(() => {//每隔一段时间刷新一次token
    newToken = takeToken();
  }, REFRESH_TIMES * 1000)
  return async function (ctx, next) {
  	//将token和创建时间写入context上下文的jwt对象中
    ctx.app.context.jwt = { token: newToken, create_at: create_at };
    const requestUrl = ctx.request.url.replace(/?.*$/gim, "");
    if (currentP.no_verify.includes(requestUrl)) {//不需要验证token的接口(GET请求去掉?及后面的参数再进行比较)
      await next();
    } else {//需要验证token的接口
      //假定客户端的token保存在请求头的authorization字段
      const authorization = ctx.request.headers.authorization || "";
      let token = "";
      if (authorization.includes("Bearer")) {//生成的jwt可能会有Bearer 前缀,需去掉在验证
        token = authorization.replace("Bearer ", "");
      } else {
        token = authorization;
      }
      try {
        await jwt.verify(token, secretkey, async (error, data) => {
          if (error) {//验证失败
            ctx.response.status = 401;
            ctx.body = { message: "token验证失败", code: -1 };
          } else {//验证成功
            ctx.append('token', newToken);//将最新的token放入响应头的token字段
            await next();
          }
        });
      } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
      }
    }
  };
}
module.exports = {
  takeToken,
  verifyToken,
  secretkey,
  CONFIG,
  REFRESH_TIMES
};

jsonwebtoken生成token使用jwt.sign(Payload, privateKey, CONFIG)方法,Payload是一个对象。需要注意的是,Payload是不加密的,所以不要存放私密信息,比如密码。

第四步:使用jsonwebtoken中间件

app.js

const Koa = require("koa");
const app = new Koa();
const { verifyToken } = require("./middleware/jwt.js")

/**添加jsonwebtoken中间件 START*/
app.use(verifyToken({ no_verify: ["/login/loginIn"] }));//登录接口不验证token
/**添加jsonwebtoken中间件 END*/

/**
	这里简要写的,app.js中的其他代码请看前面章节
	!!!需要注意的是jsonwebtoken中间件要写在路由中间件的前面,否则token不会起作用!!!
*/

第五步:新建 /module/login.js文件

第六步:编写用户登录处理接口

/module/login.js

//用户登录
async function loginUser(ctx, next) {
    const params = ctx.request.body;
    const username = params.username;
    const password = params.password;
    const sql = `SELECT id, password FROM create_user WHERE username='${username}'`;
    try {
        const r = await ctx.db.query(sql);
        let result;
        if (r && (result = r[0])) {
            if (result.password === password) {
                ctx.append('token', ctx.jwt.token);//将最新的token放入响应头的token字段
                //将用户id返回给客户端
                ctx.body = { message: '登录成功', id: result.id, code: 0 }
            } else {
                ctx.response.status = 403;
                ctx.body = { message: "密码错误", code: 1 };
            }

        } else {
            ctx.response.status = 403;
            ctx.body = { message: "未查到该用户", code: 2 };
        }
    } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
    }
}

module.exports = {
    loginUser
};

第七步:登录路由处理

同上面session处理第七步

第八步:修改 users/look 路由接口

同上面session处理第八步

第九步:前端请求、响应拦截器编写

前面【前端编写】部分已经将http请求的一些基础处理写到了http.ts中,这里只需修改一下请求和响应拦截器的代码就行:

/**请求拦截器 */
http.interceptors.request.use(function (config) {
    let token = window.localStorage.getItem('token');//查询本地存储中是否有token
    if (token) {//有token,则在请求头的authorization字段加入该token
        config.headers.authorization = token;
    }
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
/**响应拦截器 */
http.interceptors.response.use(function (response) {
    if (response.headers.token) {//响应头中是否有token字段,有则将该token存储到本地存储中
        window.localStorage.setItem('token', response.headers.token)
    }
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    if (error?.response.status === 401) {
        router.push("/login");
    }
    return Promise.reject(error.response.data);
});

除了将token存储到本地存储中,也可以存储到vue的全局变量或vuex中,看你喜好了。

至此,token登录验证就完成了!

上述token验证是依靠服务端主动刷新token,然后分派给客户端。这样有一个严重的隐患就是:一旦token被盗取,所有用户的信息都有受到威胁的隐患。

还有一种验证方式:客户端登录成功后拿到秘钥(每个用户的秘钥都不同),然后通过这个秘钥刷新token。和服务端主动刷新token不同的是,客户端需要维护token的刷新(在token失效前刷新token)。

第一、二步参照上面的服务端主动刷新token步骤

第三步:编写jsonwebtoken中间件

/middleware/jwt.js

const jwt = require("jsonwebtoken");
const secretkey = "xiaoyang";
const CONFIG = { expiresIn: 60 };
let create_at = 0;
/** 刷新token */
function takeToken(Payload = {}) {
  create_at = new Date().getTime();
  const salt = Math.random();
  return jwt.sign({ create_at: create_at, salt: salt, ...Payload }, secretkey, CONFIG);
}
/** 验证token是否有效(中间件用) */
function verifyToken(p = {}) {
  const defaultP = {
    no_verify: [],
  };
  currentP = {};
  Object.assign(currentP, defaultP, p);

  return async function (ctx, next) {
    const requestUrl = ctx.request.url.replace(/?.*$/gim, "");
    if (currentP.no_verify.includes(requestUrl)) {//不需要验证token的接口(GET请求去掉?及后面的参数再进行比较)
      await next();
    } else {//需要验证token的接口
      const authorization = ctx.request.headers.authorization || "";
      let token = "";
      if (authorization.includes("Bearer")) {
        token = authorization.replace("Bearer ", "");
      } else {
        token = authorization;
      }
      try {
        await jwt.verify(token, secretkey, async (error, data) => {
          if (error) {
            ctx.response.status = 401;
            ctx.body = { message: "token验证失败", code: -1 };
          } else {
            await next();
          }
        });
      } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
      }

    }
  };
}
module.exports = {
  takeToken,
  verifyToken,
  secretkey,
  CONFIG
};

第四步:使用jsonwebtoken中间件

app.js

const Koa = require("koa");
const app = new Koa();
const { verifyToken } = require("./middleware/jwt.js")

/**添加jsonwebtoken中间件 START*/
app.use(verifyToken({ no_verify: ["/login/loginIn", "/token/refresh"] }));//登录接口不验证token
/**添加jsonwebtoken中间件 END*/

/**
	这里简要写的,app.js中的其他代码请看前面章节
	!!!需要注意的是jsonwebtoken中间件要写在路由中间件的前面,否则token不会起作用!!!
*/

第五步:在用户表新增一个秘钥列

SQL语句

ALTER TABLE create_user ADD COLUMN secret_key VARCHAR(20) COMMENT '生成token的秘钥' DEFAULT "";

在这里插入图片描述

默认secret_key字段是空的,我们手动给他填些秘钥,以供测试:

在这里插入图片描述

第六步:新建 /module/login.js文件

第七步:编写用户登录处理接口

/module/login.js

//用户登录
async function loginUser(ctx, next) {
    const params = ctx.request.body;
    const username = params.username;
    const password = params.password;
    const sql = `SELECT id, password, secret_key FROM create_user WHERE username='${username}'`;
    try {
        const r = await ctx.db.query(sql);
        let result;
        if (r && (result = r[0])) {
            if (result.password === password) {
            	//将用户id和秘钥返回给客户端
                ctx.body = { message: '登录成功', id: result.id, secret_key: result.secret_key, code: 0 }
            } else {
                ctx.response.status = 403;
                ctx.body = { message: "密码错误", code: 1 };
            }

        } else {
            ctx.response.status = 403;
            ctx.body = { message: "未查到该用户", code: 2 };
        }
    } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
    }
}

module.exports = {
    loginUser
};

第八步:登录路由处理

同上面session处理第七步

第九步:新建 /module/token.js 文件

第十步:编写刷新token接口

/module/token.js

const jwt = require("jsonwebtoken");
const { takeToken, secretkey, CONFIG } = require("../middleware/jwt_copy2")
async function refreshToken(ctx, next) {
    const params = ctx.request.query;//GET请求的参数对象:secret_key - 用户秘钥 
    const secret_key = params.secret_key;
    const sql = `SELECT * FROM create_user WHERE secret_key='${secret_key}'`;
    try {
        const r = await ctx.db.query(sql);
        let result;
        if (r && (result = r[0])) {
            const token = takeToken({ secret_key: secret_key });//通过secret_key刷新用户的token
            try {
                await jwt.verify(token, secretkey, async (error, data) => {
                    if (error) {//刷新token失败
                        ctx.response.status = 401;
                        ctx.body = { message: "token验证失败", code: -1 };
                    } else {//刷新token成功
                        ctx.response.status = 200;
                        //响应数据:token-新token, create_at-token生成时间s, expiresIn-token有效时间周期s
                        ctx.body = { message: "token刷新成功",  token: token, create_at: data.create_at, expiresIn: CONFIG.expiresIn, code: 0 };
                    }
                });
            } catch (e) {
                ctx.response.status = 500;
                ctx.body = { message: e, code: 99 };
            }

        } else {
            ctx.response.status = 400;
            ctx.body = { message: "秘钥不正确", code: 2 };
        }
    } catch (e) {
        ctx.response.status = 500;
        ctx.body = { message: e, code: 99 };
    }
}
module.exports = {
    refreshToken
};

第十一步:创建并编写刷新token的路由

/routes/token.js

const { refreshToken } = require("../module/token")

module.exports = [
    {
        url: "/refresh",
        methods: "get",
        actions: refreshToken
    },
];

第十二步:修改前端axios基础配置文件 http.ts

http.ts

import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
import qs from "qs";
import router from '@/router/index'
interface MyAxiosInstance extends AxiosInstance {
    get<T = any, R = AxiosResponse<T>, D = any>(url: string, params?: { [propName: string]: any }, config?: AxiosRequestConfig<D>): Promise<R>;
}

const http: MyAxiosInstance = axios.create({
    baseURL: process.env.NODE_ENV === 'production' ? 'http://localhost:5152/' : '/nodeApi/',
    timeout: 60000,
});
/**改写原来的axios.get(url,[config])为axios.get(url,[params,[config]]) */
http.get = (url: string, params = {}, config = {}) => {
    let url_search = qs.stringify(params);
    if (url_search) {
        url_search = "?" + url_search;
        url = url + url_search;
    }
    return http.request({
        method: 'get',
        url: url,
        ...config
    })
}
/**刷新token
 * secret_key:用户秘钥
 */
const getToken = async (secret_key: string) => {
    return http
        .get("token/refresh", { secret_key: secret_key })
        .then((data: any) => {
            window.localStorage.setItem("token", data.token);
            return Promise.resolve(data);
        })
        .catch((err: any) => {
            return Promise.reject(err);
        });
};
/**请求拦截器 */
http.interceptors.request.use(function (config) {
    let token = window.localStorage.getItem('token')
    if (token) {
        config.headers.authorization = token;
    }
    return config;
}, function (error) {
    return Promise.reject(error);
});
/**响应拦截器 */
http.interceptors.response.use(function (response) {
    if (response.headers.token) {
        window.localStorage.setItem('token', response.headers.token)
    }
    const responseUrl = response.config?.url || "";
    //在token/refresh接口的响应时,准备(延时)刷新token
    if (responseUrl && (/token/refresh?secret_key=/gim).test(responseUrl)) {
        setTimeout(() => {
        	//在登录成功后会将secret_key存储
            const secret_key = window.localStorage.getItem("secret_key") || "";
            getToken(secret_key);
        }, (response.data.expiresIn - 30) * 1000)
    }
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    if (error?.response.status === 401) {
        router.push("/login");
    }
    return Promise.reject(error.response.data);
});

export {
    getToken
}
export default http

第十三步:登录页面编写

<script setup lang="ts">
import { reactive, ref } from "vue";
import type { FormRules, FormInstance } from "element-plus";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";
import http, { getToken } from "@/http";

const router = useRouter();
const ruleFormRef = ref<FormInstance>();
const formData = reactive({
  username: "",
  password: "",
});
const rules = reactive<FormRules>({
  username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
  password: [{ required: true, trigger: "blur", message: "请输入密码" }],
});
const submit = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      http
        .post("login/loginIn", formData)
        .then(async (data: any) => {
          if (data.code == 0) {//登录成功
          	//将用户秘钥存储起来
            window.localStorage.setItem("secret_key", data.secret_key);
            const message = data.message;
            //刷新token
            getToken(data.secret_key)
              .then((d: any) => {
                ElMessage({
                  message: message,
                  type: "success",
                });
                router.push("/index");
              })
              .catch((err: any) => {
                ElMessage({
                  message: err.message,
                  type: "error",
                });
              });
          } else {//登录失败
            ElMessage({
              message: data.message,
              type: "error",
            });
          }
        })
        .catch((err: any) => {
          ElMessage({
            message: err.message,
            type: "error",
          });
        });
    } else {
      ElMessage({
        message: "请按提示登录",
        type: "error",
      });
    }
  });
};
</script>
<!--样式和HTML省略,和上面的一样-->

一些修改

1.将前、后端项目分开

年少不谙世事,写前后端分离,没把前后端的项目文件分开,现修改前端项目全部放入web目录中,如下:

在这里插入图片描述

2.部署前端项目

打开web项目终端,输入npm run build编译,编译成功会在web目录下生成一个dist目录,即为编译好的前端项目:

在这里插入图片描述
将dist目录复制到 /server2/public/ 目录下(该目录是静态资源,不走koa路由)。开启后端服务器npm run dev
在这里插入图片描述
在浏览器输入 http://localhost:5152/dist/index.html 即可打开前端页面

参考资料:
CSDN:Vue 3中引入SCSS和LESS依赖的教程指南
CSDN:nodejs使用JWT(全)
CSDN:解决document.cookie无法获取到cookie问题
CSDN:vue3的js文件中使用router
知乎:jwt(json web token)如何做到像session一样每次操作会刷新token的过期时间?
稀土掘金:Koa2 中如何使用 koa-session 进行登陆状态管理?
稀土掘金:浅谈三种前后端可持续化访问方案 Cookie, Session, Credentials
稀土掘金:傻傻分不清之 Cookie、Session、Token、JWT
博客园:koa-session 源码浅析和理解

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。