前言
- 前端采用vue3
- 前端组件库采用ElementPlus
- 本篇文章需要结合上一篇《vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录》一起看
客户端用户注册页面
添加注册页面
添加 /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搭建前后端系统(六)-- 用户登录》中已经编写。
这样注册功能就写完了。总结一下注册流程:
- 客户端校验用户名、密码和手机号必填
- 客户端密码加密传给服务端
- 服务端同样校验用户名、密码和手机号必填,并且确定该用户未注册,然后将用户信息插入用户表
- 用户信息中,当前时间作为用户注册时间,存储的密码为客户端传来的加密密码和注册时间组合再次加密,以防被暴力破解,密钥为用户名和注册时间组合加密。其他信息跟随客户端的填写
有可能在注册时会有报错:
注册报错
原因: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"无效