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

news2024/11/24 20:08:55

前言

  • 采用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 源码浅析和理解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/663312.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL ⽀持哪些存储引擎?默认使⽤哪个?MyISAM 和 InnoDB 引擎有什么区别,如何选择?

&#x1f4a7; M y S Q L ⽀持哪些存储引擎&#xff1f;默认使⽤哪个&#xff1f; M y I S A M 和 I n n o D B 引擎有什么区别&#xff0c;如何选择&#xff1f; \color{#FF1493}{MySQL ⽀持哪些存储引擎&#xff1f;默认使⽤哪个&#xff1f;MyISAM 和 InnoDB 引擎有什么区别…

centos tomcat

利用samba共享将所用的的组件复制到/usr/java/中 并给/usr/java设置777权限 Samba配置如下 重启smb服务 进入目录开始安装jdk 在当前目录解压jdk 配置环境变量 使配置生效 查看jdk版本 创建tomcat目录 解压tomcat的安装包到当前目录 进入tomcat配置目录 启动tomcat 先关闭在启…

【微服务】微服务架构设计

文章目录 背景一、流量入口Nginx二、网关三、业务组件四、服务注册中心五、缓存和分布式锁六、数据持久层七、结构型数据存储八、消息中间件九、日志收集十、任务调度中心十一、分布式对象存储 背景 当前&#xff0c;微服务架构在很多公司都已经落地实施了&#xff0c;下面用一…

01_Linux系统安装及使用

一、安装虚拟机软件 VMware16pro 安装链接&#xff1a;https://note.youdao.com/ynoteshare/index.html?id5fc5ad640596a0fbb41a21413ada4dad&typenote&_time1687172973066 二、安装Ubuntu 64 位 Linux系统 安装链接&#xff1a;https://note.youdao.com/ynoteshar…

【动态规划】简单多状态dp问题(1)打家劫舍问题

打家劫舍问题 文章目录 【动态规划】简单多状态dp问题&#xff08;1&#xff09;打家劫舍问题1. 按摩师&#xff08;打家劫舍Ⅰ&#xff09;1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 编写代码 2. 打家劫舍Ⅱ2.1 题…

seaborn笔记:heatmap

绘制热力图 1 基本使用方法 seaborn.heatmap(data, *, vminNone, vmaxNone, cmapNone, centerNone, robustFalse, annotNone, fmt.2g, annot_kwsNone, linewidths0, linecolorwhite, cbarTrue, cbar_kwsNone, cbar_axNone, squareFalse, xticklabelsauto, yticklabelsauto, m…

C++ 教程(16)——字符串

C 字符串 C 提供了以下两种类型的字符串表示形式&#xff1a; C 风格字符串C 引入的 string 类类型 C 风格字符串 C 风格的字符串起源于 C 语言&#xff0c;并在 C 中继续得到支持。字符串实际上是使用 null 字符 \0 终止的一维字符数组。因此&#xff0c;一个以 null 结尾…

Mysql的学习笔记

目录 1、rc 级别的 mvcc 和 rr 级别的mvcc 有啥区别&#xff1f; 2、Innodb与MyIsam的文件结构&#xff1f; 3、Innodb 与MyIsam的简单对比&#xff1f; 4、innodb&#xff0c;Alter table 改字段类型&#xff0c;底层会经历什么过程&#xff1f; 5、Alter table 改字段类型…

Zebec Protocol 与 PGP 深度合作,将流支付更广泛的应用薪资支付领域

随着传统机构的入局&#xff0c;以及相关加密合规法规的落地&#xff0c;加密支付正在成为一种备受欢迎的全新支付方式。加密支付基于区块链底层&#xff0c;不受地域、时间等的限制&#xff0c;能够实时到账&#xff0c;具备去中心化、非许可等特性。 流支付是一种具备创新性的…

Jmeter和Postman做接口测试的区别你知道吗

区别1:用例组织方式 不同的目录结构与组织方式代表不同工具的测试思想&#xff0c;学习一个测试工具应该首先了解其组织方式。 Jmeter的组织方式相对比较扁平&#xff0c;它首先没有WorkSpace&#xff08;工作空间&#xff09;的概念&#xff0c;直接是TestPlan&#xff08;测…

搜索旋转排序数组(leetcode 33)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路思路复杂度分析 5.实现示例参考文献 1.问题描述 整数数组按升序排列&#xff0c;数组中的值互不相同 。 假设数组在预先未知的某个点上进行了旋转。 如数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2]。 搜索一个给定的目…

如何安装Nginx服务

目录 一、Nginx服务 Nginx的并发能力受影响因素 二、Nginx与Apache的差异 Nginx相对于Apache的优点 Apache相对于Nginx的优点 三、阻塞/非阻塞 四、同步/异步 Nginx应用场景 五、编译安装Nginx服务 关闭防火墙和安全机制 安装依赖环境 将相关包导入/opt当中 进入指…

Linux分区的基本概念。

文章目录 前言 一、分区概念 1&#xff0c;书名&#xff0c;主引导纪录&#xff08;&#xff2d;&#xff22;&#xff32;&#xff09; 2&#xff0c;正文&#xff0c;就是硬盘中纪录的数据。 3&#xff0c;索引相当于硬盘中的分区表 3.1主分区&#xff08;存放地址&#xff0…

React、Vue项目build打包编译后如何再修改后台请求地址

vue项目大家都了解&#xff0c;开发用 npm run dev/npm run serve。而要上线则必须是先将项目打包编译 npm run build 之后成为了普通的静态网页才可上线进行部署及发布。同样这时候我们也已经将代码全部写好了。如果说要改里面的某个值或者修改请求地址我们应该怎么办呢&#…

IDEA中创建编写JSP

一、安装Tmocat并配置环境 安装请参考&#xff1a;https://www.cnblogs.com/weixinyu98/p/9822048.html 安装请参考&#xff1a;https://www.cnblogs.com/zhanlifeng/p/14917777.html 注意&#xff1a;在安装成功Tomcat测试是否成功安装时&#xff0c;访问“http://localhost:8…

selenium自动化教程及使用java来爬取数据

目录 一、介绍二、下载浏览器驱动1.获取要下载的驱动版本号2.下载驱动 三、Maven如下四、简单使用五、定位器1.定位器2.说明(1) class name 定位器(2) css selector 定位器(3) id 定位器(4) name 定位器(5) link text 定位器(6) partial link text 定位器(7) tag 定位器(8) xpa…

Android 内存检测LeakCanary

在github上下载了一个项目&#xff1a;安装debug版本会产生两个apk&#xff0c;一个是apk本身&#xff0c;一个是Leaks release版本就正常 不会产生这个问题&#xff0c;百思不得其解&#xff0c;第一次遇到这个问题。 看到这篇博客豁然开朗&#xff1a;在build.gradle.kts 里…

3天爆肝整理,性能测试问题汇总+解决办法(重要)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 开始性能测试前需…

入门孪生网络3-------使用一维卷积神经网络1DCNN与孪生网络的组合模型来实现excel数据的分类

文章目录 前言入门孪生网络的第三小节&#xff0c;我尝试使用keras或tensorflow2框架来搭建一个数据分类的网络。大家可以参考的程序思路&#xff0c;我也是小白&#xff0c;可以评论区一起讨论。 一、孪生网络与1DCNN组合网络的搭建思路二、我编写的孪生网络与1DCNN组合网络程…

操作教程:EasyCVR视频融合平台如何配置平台级联?

EasyCVR视频融合平台基于云边端一体化架构&#xff0c;可支持多协议、多类型设备接入&#xff0c;在视频能力上&#xff0c;平台可实现视频直播、录像、回放、检索、云存储、告警上报、语音对讲、电子地图、集群、智能分析以及平台级联等。平台可拓展性强、开放度高、部署轻快&…