Vue+Node.js+MySQL+Element-Plus实现一个账号注册与登录功能
注册登录功能
- 前端部分
- Vite快速构建vue项目
- 引入Element-Plus组件库
- 静态登录页面
- 使用正则表达式进行表单校验
- 动态绑定表单数据
- 点击登录和注册按钮时的逻辑处理
- 登录按钮
- 注册按钮
- 后端部分
- 连接数据库
- Express搭建服务器
- 后端接口
- 注册接口
- 登录接口
- 测试token接口
- 优化
- 添加路由守卫
- 我遇到的问题
- 效果展示
- 准备工作:先理清整体思路,明白各功能的实现思路
- 使用Vue框架以及element-plus组件库来写前端页面
- 使用Node.js来写后端接口以及连接数据库
- 使用MySQL来存储用户信息
前端部分
Vite快速构建vue项目
- 我这里使用了
Vite
来快速构建了一个vue
项目,构建命令如下:
npm create vite@latest my-vue-app -- --template vue
引入Element-Plus组件库
- Element-Plus官网中也有更加详细的引入流程
- 这里我采用的是按需自动导入element组件
- 首先需要安装
unplugin-vue-components
和unplugin-auto-import
这两款插件,命令如下: npm install -D unplugin-vue-components unplugin-auto-import
- 再将下面的代码插入
Vite
的配置文件中即可
// vite.config
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
静态登录页面
- 接下来是写出了登录页面的静态页面,这个地方因人而异,大家可以写出自己喜欢的页面,效果如下:
使用正则表达式进行表单校验
- 这里我先分别定义了账号和密码的正则表达式
- 账号:用于验证用户名是否只包含字母(大小写均可)和数字,账号里面的
+
表示账号不能为空 - 密码:用于验证密码是否只包含字母(大小写均可)、数字和下划线,并且长度在 2 到 16 个字符之间。
- 账号:用于验证用户名是否只包含字母(大小写均可)和数字,账号里面的
- 然后在表单变化的回调函数中进行校验
//用户名变化回调函数
const onUsernameChange = (event) => {
// console.log(event);
if (!reg_username.test(event)) {
ElMessage({
message: "用户名格式不对,只能是字母和数字",
type: "warning",
plain: true,
});
}
};
//密码变化回调函数
const onPasswordChange = (event) => {
if (!reg_password.test(event)) {
ElMessage({
message: "密码格式不对,只能包含字母、数字、下划线",
type: "warning",
plain: true,
});
}
};
//定义正则表达式
const reg_username = /^[a-zA-Z0-9]+$/;
const reg_password = /^[a-zA-Z0-9_]{2,16}$/;
动态绑定表单数据
- 这里我用到了
vue
中的ref
响应式数据,将username
和password
都定义为ref
类型
import { ref } from "vue";
const username = ref("");
const password = ref("");
- 并将
username
和password
通过v-model
动态绑定到表单中
//账号
<el-input
@change="onUsernameChange"
label="用户名"
v-model="username" //绑定数据
style="width: 240px; margin-left: 10px"
placeholder="请输入用户名"
/>
//密码
<el-input
@change="onPasswordChange"
label="密码"
v-model="password" //绑定数据
type="password"
style="width: 240px; margin-left: 10px"
placeholder="请输入密码"
:show-password="true"
/>
点击登录和注册按钮时的逻辑处理
登录按钮
- 首先点击登录按钮后,会拿到此时表单中的账号和密码,然后通过
axios
向后端发送网络请求并携带此时的表单数据 - 再通过后端返回的信息来进行后续的处理,后续情况无非是下面两种情况
- 用户不存在:提示用户账号不存在请先前去注册
- 用户存在:
- 如果密码错误,则提示用户密码输入错误,请重新输入
- 如果密码正确,那么此时后端会返回该用户的
token
信息,前端需要做的就是将后端返回的token
信息存放到localstorage
中,这里我采用的是存放到localstorage
中,当然存放到别的地方也行。
注意:密码正确后端返回的status为0
用户不存在或者密码错误后端返回的status为1
//点击登录按钮的回调函数
const onLogin = async ()=>{
//获取此时的表单数据
const data = {
username:username.value,
password:password.value
}
//携带参数对后端进行网络请求
const res = await axios({
method:'post',
url:'http://127.0.0.1:8080/api/login',
data:{
username:username.value,
password:password.value
}
})
//用户不存在||用户存在但密码不正确
if(res.data.status){
ElMessage({
message: res.data.message,
type: "error",
plain: true,
});
}else{
//用户存在
ElMessage({
message:res.data.message,
type:"success",
plain:true
})
//拿到token信息
console.log(res);
const my_token = res.data.token
//将token存放到localstorage中
localStorage.setItem('token',my_token)
//跳转路由,跳转到主页面
router.push('/home')
}
}
注册按钮
- 点击注册按钮后,同样向后端发送请求,根据请求进行前端的渲染
- 注册按钮的前端逻辑比起登录按钮更加简单,只有两种情况
- 数据库中该用户已存在:提示用户该用户已经注册过了
- 数据库中没有该用户:提示用户注册成功。
//点击注册按钮的回调函数
const onRegister =async()=>{
//获取此时的账号和密码
const data = {
username:username.value,
password:password.value
}
//发起注册请求
const res = await axios({
method:'post',
url:'http://127.0.0.1:8080/api/register',
data:{
username:data.username,
password:data.password
}
})
//console.log(res);
if(res.data.status){
//该账号已经被注册
ElMessage({
message:res.data.message,
type:"error",
plain:true
})
}else{
//账号注册成功
ElMessage({
message:res.data.message,
type:"success",
plain:true
})
}
}
后端部分
- 这里我采用的是Node.js中的Express框架来进行Web服务器的搭建
连接数据库
- 使用Node.js中的
mysql
模块进行连接数据库
//导入MySQL
import mysql from 'mysql'
//创建数据库连接对象
const db = mysql.createPool({
host:'127.0.0.1',
user:'root',
password:'*******', //user对应的密码
database:'*****' //数据库名称
})
//向外共享数据库连接对象
export default db
Express搭建服务器
- 在这一部分需要注册路由、配置中间件、测试数据库连接、监听端口
- 需要用到的一些中间件:
- CORS中间件:解决跨域资源问题
- expressJWT中间件:解析token的中间件
- express.urlencoded中间件:解析表单数据的中间件
- 自定义中间件:验证token有效性的中间件
- 错误中间件:判断登录成功后的请求是否携带token
下面的代码中的资源引入路径是我这个项目中的路径,需要更改
import express from 'express'
import cors from 'cors'
import db from '../db/index.js'
import router from './router/user.js'
import expressJWT from 'express-jwt'
const app = express()
//将CORS注册为全局中间件
app.use(cors())
//配置解析token的中间件
app.use(expressJWT({secret:"yaoyao No1"}).unless({path:[/^\/api\//]}))
//配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
app.use(express.json());
//导入并注册路由
app.use(router)
//验证token的有效性的中间件
app.use((req,res,next)=>{
const authHeader = req.headers['Authorization']
const token = authHeader && authHeader.split(' ')[1]
console.log('token:' + token);
if(!token){
//如果token不存在
return res.status(1).json({error:'No Token Provided'})
}
//验证token有效性
jwt.verify(token,(err,user)=>{
if(err){
return res.status(1).json({error:'Invalid Token'})
}
//将用户信息附加到请求对象
req.user = user
next()
})
})
//配置错误中间件
app.use((err,req,res,next)=>{
if(err.name === 'UnauthorizedError'){
return res.send({
status:1,
message:'身份认证失败'
})
}
})
// 测试数据库连接
db.query('select 1',(err,result)=>{
if(err){
return console.log(err);
}
console.log(result);
})
//监听
app.listen(8080,()=>{
console.log('server running at http://127.0.0.1:8080');
})
后端接口
- 在这一部分写前端需要用到的接口,其中包括用户注册接口、用户登录接口以及获取用户头像的接口(测试携带token的接口)
注册接口
- 注册接口逻辑比较简单,使用
SQL
语句查找数据库并向其中插入数据即可实现。 - 不过将插入数据时,不能将用户密码以明文的方式插入,而应该先对密码进行加密,这样可以提高安全性。这里我是通过
bcrypt
对密码进行哈希盐加密
,这样的加密方式有唯一性
,即使两个用户的密码完全相同,但是由于盐值不同,所以在数据库中的密码也不相同。
登录接口
- 登录接口则需要使用
bcrypt
中的匹配方法来进行验证用户输入的密码是否为数据库中的密码一致。如果一致,则登录成功,跳转到主页;如果不一致,则给用户轻提示。
测试token接口
- 点击后可以获取该用户的头像,并将其渲染到主页面中
import express from "express";
import db from "../../db/index.js";
import jwt from "jsonwebtoken";
import bcrypt from 'bcryptjs'
//创建路由对象
const router = express.Router();
//注册用户
router.post("/api/register", (req, res) => {
const body = req.body;
//首先查询数据库中是否存在该用户名
db.query(
"select * from ev_users where username = ?",
body.username,
(err, result) => {
if (err) {
return console.log(err);
}
if (result.length > 0) {
//用户存在
res.send({
status: 1,
message: "该账号已经注册过了",
});
} else {
//对用户的密码进行加密
const hashPassword = bcrypt.hashSync(body.password,10)
// console.log('hashpassword:' + hashPassword);
//用户不存在,将其存放到数据库中
db.query(
"insert into ev_users (username,password) values (?,?)",
[body.username, hashPassword],
(err, result) => {
if (err) {
return console.log(err);
}
// console.log("存储成功" + result);
}
);
res.send({
status: 0,
message: "账号注册成功",
});
}
}
);
});
//登录接口
router.post("/api/login", (req, res) => {
//拿到了前端表单中的数据
const data = req.body;
//首先进行数据库查询是否有该用户
db.query(
"select * from ev_users where username = ?",
data.username,
(err, result) => {
if (err) {
return console.log(err);
}
if (result.length > 0) {
//用户存在
//获取数据库中加密后的密码
const hash_password = result[0].password
//验证密码
const isMatch = bcrypt.compareSync(data.password,hash_password)
//进行密码匹配
if (isMatch) {
//生成用户token
const userInfo = {
...result[0],
password:'',
};
//密钥
const secretKey = "yaoyao No1";
//生成token
const token = jwt.sign(userInfo, secretKey, { expiresIn: "1h" });
res.send({
status: 0,
message: "登录成功",
token:'Bearer ' + token
});
} else {
res.send({
status: 1,
message: "密码错误,请重新输入",
});
}
} else {
//用户不存在
res.send({
status: 1,
message: "该账号不存在,请前去注册",
});
}
}
);
});
//携带token参数的请求测试
router.get('/my/info',(req,res)=>{
//查询数据库拿到用户头像
const userId = req.user.id
db.query('select * from ev_users where id = ?',userId,(err,result)=>{
if(err){
return console.log(err);
}
//创建一个新对象,排除特定的键值对
const newresult = {...result[0]}
delete newresult.password
res.send({
status:0,
message:'用户信息获取成功',
data:newresult
})
})
})
//将路由对象共享出去
export default router;
优化
添加路由守卫
- 只有携带
token
的用户才可以访问主页,否则如果没有token
的话,即使输入主页的url也会跳转到登录页面 - 实现原理:使用路由前置守卫进行判断,如果当前页面中存在
token
,那么就可以访问主页,否则,就跳转到登录页面。
//设置路由守卫
router.beforeEach(async (to,from,next)=>{
console.log('前置路由守卫触发');
const token = await localStorage.getItem('token')
if(!token){
//如果token不存在
if(to.path !== '/login'){
//如果目标页面不是登录页
next('/login')
}else{
next()
}
}else{
//如果token存在
next()
}
})
我遇到的问题
- 我刚开始在添加路由守卫后,没有进行判断当前跳转的页面是否为
/login
页面,然后就导致了没有token
,即使当前已经在登录页面,页面就会一直进行跳转,类似于使用了递归函数但是却没有递归出口,就很尴尬,需要避免