vue+Nodejs+Koa搭建前后端系统(七)-- 用户注册、注销

9 篇文章 0 订阅

前言

客户端用户注册页面

添加注册页面

添加 /src/pages/register/register.vue 文件
在这里插入图片描述

安装md5

md5是加密插件,用于密码加密。安装md5

npm install --save js-md5

编写注册页面

register.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";
import md5 from "js-md5";

const router = useRouter();
//form表单的Ref(猜测:相当于VNode,继承ElementDom)
const formRef = ref<FormInstance>();
//提交表单时的参数
const formData = reactive({
  username: "",
  password: "",
  sex: "1",
  mobile: "",
  birth: null,
  email: "",
});
//表单校验
const rules = reactive<FormRules>({
  username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
  password: [{ required: true, trigger: "blur", message: "请输入密码" }],
  mobile: [{ required: true, trigger: "blur", message: "请输入手机号" }],
});
//ElementPlus时间组件可选日期范围函数(参看DatePicker组件disabled-date属性)
const disabledDate = (D: Date) => {
  return D.getTime() > new Date().getTime();
};
//提交表单
const register = (formEl: FormInstance | undefined) => {
  formEl?.validate((valid, fields) => {
    if (valid) {//通过校验-向后端请求注册接口/users/register
      http
        .post("/users/register", { ...formData, password: md5(formData.password) })
        .then(async (data: any) => {
          if (data.code == 0) {
            ElMessage({
              message: data.message,
              type: "success",
            });
          } else {
            ElMessage({
              message: data.message,
              type: "error",
            });
          }
        })
        .catch((err: any) => {
          ElMessage({
            message: err.message,
            type: "error",
          });
        });
    } else {
      ElMessage({
        message: "请按提示填写信息",
        type: "error",
      });
    }
  });
};
const goLogin = () => {
  router.replace("/login");
};
</script>
<template>
  <div class="register">
    <el-form
      class="form"
      ref="formRef"
      :model="formData"
      :rules="rules"
      label-width="5em">
      <h2>用户注册</h2>
      <el-form-item prop="username" label="用户名">
        <el-input
          v-model="formData.username"
          name="register"
          placeholder="请输入用户名"
          autocomplete="new-password" />
      </el-form-item>
      <el-form-item prop="password" label="密码">
        <el-input
          v-model="formData.password"
          type="password"
          name="register"
          autocomplete="new-password"
          placeholder="请输入密码"
          show-password />
      </el-form-item>
      <el-form-item prop="sex" label="性别">
        <el-radio-group v-model="formData.sex">
          <el-radio label="1"></el-radio>
          <el-radio label="0"></el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item prop="mobile" label="手机">
        <el-input v-model="formData.mobile" placeholder="请输入手机号" />
      </el-form-item>
      <el-form-item prop="birth" label="出生日期">
        <el-date-picker
          v-model="formData.birth"
          type="date"
          value-format="YYYY-MM-DD"
          :disabled-date="disabledDate"
          placeholder="请选择出生日期" />
      </el-form-item>
      <el-form-item prop="email" label="邮箱">
        <el-input v-model="formData.email" placeholder="请输入邮箱" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="register(formRef)">注册</el-button>
        <el-button @click="goLogin">返回登录页</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
<style lang="less" scoped>
.register {
  .form {
    max-width: 400px;
    box-sizing: border-box;
    margin: auto;
    padding: 10px 40px;
  }
}
</style>

页面效果
在这里插入图片描述
在这里有个小插曲:我在编写注册页时,用户名和密码输入框总是自动填充我之前登录过的信息,查了一些博客,原因找到了。是因为注册页和登录页的的用户名和密码在el-form组件的位置和某些关键属性(如prop)一样,所以被自动填充了。解决办法也有好多,但一一试验,只有在el-input组件添加 autocomplete="new-password"属性才有效。

添加注册页面路由

/src/router/index.ts部分代码

{
    path: "/register",
    name: "Register",
    component: () => import("@/pages/register/register.vue"),
}

在这里插入图片描述

添加注册按钮

在登录页(/src/pages/login/login.vue)面添加注册按钮

<div class="register-btn-wrap">
	<el-button type="text" size="middle" @click="register">注册</el-button>
</div>

点击该按钮进入注册页面

<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
const register = () => {
  router.replace("/register");
};
</script>

另外,上一篇《vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录》中,登录接口入参需要小小修改一下,即把密码加密

/**login.vue部分代码*/
import { reactive, ref } from "vue";
import http, { getToken } from "@/http";
import md5 from "js-md5";
const formData = reactive({
  username: "",
  password: "",
});
http.post("login/loginIn", {...formData,password: md5(formData.password)}).then(async (data: any) => {
/**此处省略700左右个字*/
}).catch((err: any) => {
/**此处省略80左右个字*/
});

服务端用户注册接口

新增用户表列

SQL语句

ALTER TABLE create_user 
    ADD COLUMN  mobile VARCHAR(13) COMMENT '手机号' NOT NULL,
    ADD COLUMN sex ENUM('1','0') COMMENT '性别:1-男 0-女' DEFAULT '1',
    ADD COLUMN email VARCHAR(20) COMMENT '邮箱' DEFAULT '',
    ADD COLUMN birth DATE COMMENT '生日' DEFAULT NULL;

vscode MySQL插件操作步骤
在这里插入图片描述

Nodejs加密模块

crypto是Nodejs内置的加密模块,可以新建 /plugins/crypto.js 文件,编写一个加密函数,用以方便以后调取

const crypto = require('crypto');
module.exports = function (t) {
    return crypto.createHash('md5').update(t).digest('hex')
}

Nodejs安装dayjs

npm install dayjs

dayjs中国镜像站点

添加注册路由

/routes/users.js

const { registerUser } = require("../module/user");
module.exports = [
  {
    url: "/register",
    methods: "post",
    actions: registerUser,
  }
];

编写注册接口

/module/user.js

const md5 = require('../plugins/crypto');
const dayjs = require('dayjs');

//注册用户
async function registerUser(ctx, next) {
  const params = ctx.request.body;
  const sql = `SELECT * FROM create_user WHERE username='${params.username}'`;
  try {
    const r = await ctx.db.query(sql);
    if (r && r[0]) {//该用户名已注册过
      ctx.response.status = 403;
      ctx.body = { message: "该用户已注册", code: 1 };
    } else {//该用户名未注册过
      if (!params.username) {
        ctx.response.status = 403;
        ctx.body = { message: "请输入用户名", code: 2 };
      } else if (!params.password) {
        ctx.response.status = 403;
        ctx.body = { message: "请添加密码", code: 3 };
      } else if (!params.mobile) {
        ctx.response.status = 403;
        ctx.body = { message: "请添加手机号", code: 4 };
      } else {//验证通过
        const nowD = dayjs();
        //当前时间,即用户注册时间:YYYY-MM-DD HH:mm:ss格式
        const nowFormat = nowD.format("YYYY-MM-DD HH:mm:ss");
        //生日:YYYY-MM-DD格式
        const birth = params.birth ? dayjs(params.birth).format("YYYY-MM-DD") : "";
        //以【用户名】+【用户注册时间】加密生成秘钥
        const secret_key = md5(params.username + nowFormat);
        //以【密码】+【用户注册时间】加密生成密码(加用户注册时间是为了提高密码的安全性,避免被暴力破解)
        const password = md5(params.password + nowFormat);
        //INSERT到数据库表的列、值集合
        const p = {
          username: params.username,
          password: password,
          create_time: nowFormat,
          secret_key: secret_key,
          mobile: params.mobile,
          sex: params.sex,
          email: params.email,
          birth: birth
        };
        //将列、值集合中值为空的过滤掉,返回过滤后的key集合
        const keys = Object.keys(p).filter(key => p[key] || p[key] === 0);
        //将过滤后的集合整理成mysql插件需要的列格式
        const columns = keys.join(",");
        //将过滤后的集合整理成mysql插件需要的值格式
        const values = keys.map(key => typeof p[key] === "string" ? `'${p[key]}'` : p[key]).join(",");
        const sql = `INSERT INTO create_user (${columns}) value(${values})`;
        try {
          const r = await ctx.db.query(sql);
          ctx.response.status = 200;
          //注册成功:将用户id和秘钥放入响应中
          ctx.body = { message: "注册成功", code: 0, data: { id: r.insertId, secret_key: secret_key } };
        } catch (e) {
          ctx.response.status = 500;
          ctx.body = { message: e, code: 99 };
        }
      }
    }
  } catch (e) {
    ctx.response.status = 500;
    ctx.body = { message: e, code: 99 };
  }
}
module.exports = {
  registerUser
};

绕过登录验证

注册接口 /users/register 应该绕过登录验证,在app.js中

/**部分代码*/
const app = new Koa();
const { verifyToken } = require("./middleware/jwt.js");
// const { takeSession, verifySession } = require("./middleware/session.js")

/**如果用token进行登录验证*/
app.use(verifyToken({ no_verify: ["/login/loginIn", "/token/refresh", "/users/register"] }));
/**如果用session进行登录验证*/
// app.use(takeSession(app));
// app.use(verifySession({ no_verify: ["/login/loginIn", "/users/register"] }))

verifyToken 中间件在上一篇《vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录》中已经编写。

这样注册功能就写完了。总结一下注册流程:

  1. 客户端校验用户名、密码和手机号必填
  2. 客户端密码加密传给服务端
  3. 服务端同样校验用户名、密码和手机号必填,并且确定该用户未注册,然后将用户信息插入用户表
  4. 用户信息中,当前时间作为用户注册时间,存储的密码为客户端传来的加密密码和注册时间组合再次加密,以防被暴力破解,密钥为用户名和注册时间组合加密。其他信息跟随客户端的填写

有可能在注册时会有报错:

注册报错
在这里插入图片描述
原因:password或secret_key列的字符串长度超过了该列设置的最大长度
修改:将报错字段的类型长度修改大一些
在这里插入图片描述
修改列的SQL语句:

ALTER TABLE create_user MODIFY COLUMN password VARCHAR(64) NOT NULL COMMENT "用户密码";
ALTER TABLE create_user MODIFY COLUMN secret_key VARCHAR(64) DEFAULT "" COMMENT "密钥";

用户注销

根据登录方式不同,注销也有不同的方式

session登录方式的注销

第一步:服务端编写注销接口

/module/login.js 注销代码:

async function loginUser(ctx, next) {
/**此处省略n段代码*/
}
async function logoutUser(ctx, next) {
    ctx.session = null;
    ctx.body = { message: '登出成功', code: 0 }
}
module.exports = {
    loginUser,
    logoutUser
};

第二步:添加服务端注销路由

/routes/login.js

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

第三步:客户端编写注销

/src/pages/index.vue

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

const router = useRouter();
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) => {
      ElMessage({
        message: err.message,
        type: "error",
      });
    });
  isload.value = false;
};
/**注销*/
const logout = async () => {
  await http.post("login/logout").then((data: any) => {
      if (data.code == 0) {
        ElMessage({
          message: data.message,
          type: "success",
        });
        router.push("/login");
      }
    }).catch((err: any) => {
      ElMessage({
        message: err.message,
        type: "error",
      });
    });
};
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>
    <el-button class="refresh-btn" @click="logout">注销</el-button>
  </div>
</template>

服务端主动刷新token登录方式的注销

/src/pages/index.vue注销代码

import { useRouter } from "vue-router";
const router = useRouter();
const logout = async () => {
  window.localStorage.removeItem("token");
  router.push("/login");
};

客户端主动刷新token登录方式的注销

/src/pages/index.vue注销代码

const logout = async () => {
  window.localStorage.removeItem("secret_key");
  window.localStorage.removeItem("token");
  window.location.href = `/#/login`;
};

这里只需要客户端处理就可以,客户端删除本地存储token和secret_key,然后导航到登录页。

为什么用window.location.href导航到登录页,而不是vue-router?还记得上一篇文章客户端主动刷新token登录么,其通过setTimeout不断请求刷新token方法
在这里插入图片描述
如果用vue-router,其虽然导航到了登录页,但setTimeout中的方法依然在等待执行。当你再次登录成功,上一个setTimeout延时器未关闭,而一个新的延时器又被开启。

当然,可以定义一个全局变量或者是vue store用来存储延时器Id,然后在注销时清除该延时器

http.ts请求拦截

let w = window as any;
//延时器Id
w.timerId = null;
/**响应拦截器 */
http.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    if (response.headers.token) {
        window.localStorage.setItem('token', response.headers.token)
    }
    const responseUrl = response.config?.url || "";
    //刷新token
    if (responseUrl && (/token\/refresh\?secret_key=/gim).test(responseUrl)) {
        w.timerId = setTimeout(() => {
            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);
});

/src/pages/index.vue注销代码

import { useRouter } from "vue-router";
const router = useRouter();
const logout = async () => {
  const w = window as any;
  //清除延时器
  if(w.timerId) clearTimeout(w.timerId);
  window.localStorage.removeItem("secret_key");
  window.localStorage.removeItem("token");
  router.push("/login");
};

参考资料:
简书:Chrome禁用自动填充autocomplete="off"无效

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值