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

2023-11-06 15:51

本文主要是介绍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);//序列化paramsif (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) {// 对响应错误做点什么// 这里假定在服务器验证登录身份失败后会抛出错误,并将状态码改为401if (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-inputv-model="formData.password"type="password"autocomplete="off"/></el-form-item><el-form-item><el-buttonclass="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 最大存活周期, 单位 msautoCommit: 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验证-服务端主动刷新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();//最新的tokensetInterval(() => {//每隔一段时间刷新一次tokennewToken = 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');//查询本地存储中是否有tokenif (token) {//有token,则在请求头的authorization字段加入该tokenconfig.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失效前刷新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刷新用户的tokentry {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有效时间周期sctx.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接口的响应时,准备(延时)刷新tokenif (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;//刷新tokengetToken(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 源码浅析和理解

这篇关于vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:https://blog.csdn.net/weixin_39256994/article/details/130682476
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/357581

相关文章

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

Vue3使用router,params传参为空问题

《Vue3使用router,params传参为空问题》:本文主要介绍Vue3使用router,params传参为空问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录vue3使用China编程router,params传参为空1.使用query方式传参2.使用 Histo

CSS Padding 和 Margin 区别全解析

《CSSPadding和Margin区别全解析》CSS中的padding和margin是两个非常基础且重要的属性,它们用于控制元素周围的空白区域,本文将详细介绍padding和... 目录css Padding 和 Margin 全解析1. Padding: 内边距2. Margin: 外边距3. Padd

CSS will-change 属性示例详解

《CSSwill-change属性示例详解》will-change是一个CSS属性,用于告诉浏览器某个元素在未来可能会发生哪些变化,本文给大家介绍CSSwill-change属性详解,感... will-change 是一个 css 属性,用于告诉浏览器某个元素在未来可能会发生哪些变化。这可以帮助浏览器优化

CSS去除a标签的下划线的几种方法

《CSS去除a标签的下划线的几种方法》本文给大家分享在CSS中,去除a标签(超链接)的下划线的几种方法,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧... 在 css 中,去除a标签(超链接)的下划线主要有以下几种方法:使用text-decoration属性通用选择器设置:使用a标签选择器,将tex

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

Python将博客内容html导出为Markdown格式

《Python将博客内容html导出为Markdown格式》Python将博客内容html导出为Markdown格式,通过博客url地址抓取文章,分析并提取出文章标题和内容,将内容构建成html,再转... 目录一、为什么要搞?二、准备如何搞?三、说搞咱就搞!抓取文章提取内容构建html转存markdown

在React中引入Tailwind CSS的完整指南

《在React中引入TailwindCSS的完整指南》在现代前端开发中,使用UI库可以显著提高开发效率,TailwindCSS是一个功能类优先的CSS框架,本文将详细介绍如何在Reac... 目录前言一、Tailwind css 简介二、创建 React 项目使用 Create React App 创建项目

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.