登录注册的前后端实现(vue + express + Mysql )
1. 安装 Mysql 并创建一个保存用户数据的表
-
安装 navicat: Navicat Premium 16 永久破解激活 - 酷酷的阿杰 - 博客园 (cnblogs.com)
-
启动 Mysql: (59条消息) Express 实战: 连接 MySQL 数据库_超悠閒的博客-CSDN博客
MySQL 目录下的 bin 目录中运行 net start mysql 启动服务器 mysql -u root -p 之后输入密码 修改密码: alter user 'root'@'localhost' identified with mysql_native_password by 'xxxxx'; -
创建数组库、数据表、填充基本数据:
新建 express_mysql_db.sql 文件,统一创建数据库 express_mysql_db、数据表 user,并填充基本数据
# 创建空数据库 DROP DATABASE IF EXISTS express_mysql_db; CREATE DATABASE express_mysql_db; USE express_mysql_db; # 创建 user 表 CREATE TABLE `user` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `password` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8; # 插入测试数据 INSERT INTO user (`name`, `password`) VALUES ('xxx', 'xxx'); -
导入数据: 使用 .sql 文件将指令脚本化,在 mysql CLI 里面一键导入
- ⌊ 导入sql文件 ⌉:source express_mysql_db.sql
- ⌊ 查看数据库中的所有表 ⌉:show tables
- ⌊ 查看 user 表的列说明 ⌉:show columns from user
- ⌊ 查看 user 表的所有信息 ⌉:select * from user
mysql> source xxxx/express_mysql_db.sql Query OK, 0 rows affected, 1 warning (0.00 sec) Query OK, 1 row affected (0.00 sec) Database changed Query OK, 0 rows affected, 2 warnings (0.01 sec) Query OK, 1 row affected (0.00 sec) mysql> show tables; +----------------------------+ | Tables_in_express_mysql_db | +----------------------------+ | user | +----------------------------+ 1 row in set (0.01 sec) mysql> show columns from user; +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | password | varchar(255) | NO | | NULL | | +----------+--------------+------+-----+---------+----------------+ 3 rows in set (0.00 sec) mysql> select * from user; +----+-------------+------------+ | id | name | password | +----+-------------+------------+ | 1 | xxxxxxxxxxx | xxxxxxxxxx | +----+-------------+------------+ 1 row in set (0.00 sec)
2. Vue 中实现 token 用户登录
👉 参考: (59条消息) vue获取token 实现token登录vue token小菜鸟学代码··的博客-CSDN博客
-
在项目中使用 vuex 添加保存和删除 token 的全局方法
-
index.js
//引入Vuex import { createStore } from 'vuex' import state from './state' import mutations from './mutations' export default createStore({ state, mutations, }) -
state.js
export default { // 登录认证,已登录isLogin: '1',未登录 '0' isLogin: '0', token: localStorage.getItem('token') ? localStorage.getItem('token') : '' } -
mutations.js
export default { // 设置存储token setToken(state, value) { state.token = value state.isLogin = '1' try { localStorage.token = value localStorage.isLogin = '1' } catch (error) { } }, // 删除token removeStorage(state, value) { state.isLogin = '0' try { localStorage.removeItem('token') localStorage.isLogin = '0' } catch (error) { } } }
-
-
在登录页面 (login.vue) 中登录方法调用接口成功后把 token 存储在本地存储中 localStorage。这里使用了 vant 组件库
<script setup> import router from "@/router"; import store from "@/store"; import axios from "axios"; import { showSuccessToast, showFailToast } from "vant"; import { ref } from "vue"; // 判断是不是电话号码 const pattern = /^1[34578]\d{9}$/g; const tel = ref(""); const password = ref(""); // 登录函数 const onSubmit = (values) => { // 预防一部分 XSS 攻击 password.value = password.value.replace(/</g, "<").replace(/>/g, ">"); axios .post("/api/login", { username: tel.value, userPwd: password.value, }) .then((res) => { if (res.data.status === 200) { store.commit("setToken", res.data.token); showSuccessToast(res.data.message); router.push({ name: "Home" }); } else { showFailToast(res.data.message); tel.value = ""; password.value = ""; router.push({ name: "Login" }); } }) .catch((err) => { showFailToast(err); }); }; // 退出登录 const onReturn = () => { confirm("退出登录?"); tel.value = ""; password.value = ""; store.commit("removeStorage"); // 清除登录信息 router.push({ path: "./login", }); showSuccessToast("退出成功"); }; -
在 main.js 中添加请求拦截器,并在请求头中添加 token。
- ⌊ router.beforeEach ⌉:在需要登录才可以访问的页面设置前置路由守卫
- ⌊ axios.interceptors.request.use ⌉:设置请求拦截器,对需要鉴权的请求的请求头带上 token
router.beforeEach((to, from, next) => { if (to.meta.isAuth) { // 需要登录 if (window.localStorage.token && window.localStorage.isLogin === '1') { next() } else if (to.path !== '/login') { let token = window.localStorage.token if (token === 'null' || token === '' || token === undefined) { next({ path: '/login' }) showFailToast('请登录后操作 ') } } else { next() } } else { // 不需要登录 next() } }) //添加请求拦截器 axios.interceptors.request.use( config => { // 在发送请求之前做些什么,这里只对需要进行 token 鉴权的请求进行了请求拦截 if (config.url === '/api/detail.json' && store.state.token) { // 给请求头加 token,配合后端的 express-jwt config.headers.Authorization = 'Bearer ' + store.state.token } return config; }, error => { // 对请求错误做什么 return Promise.reject(error); }) //http reponse响应拦截器 axios.interceptors.response.use( response => { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么 return response; }, error => { if (error.response) { switch (error.response.status) { case 401: localStorage.removeItem('token'); router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } //登录成功后跳入浏览的当前页面 }) } } }) -
需要鉴权才能发送请求的组件,需要结合后端的响应对一些情况进行处理
<script setup> const getDetailInfo = () => { axios .get("/api/detail.json", { params: { id: route.params.id, }, }) .then((res) => { const resData = res.data; if (resData.status === 401) { // token 失效,跳转到登录页面重新登录 router.push({ name: "Login", }); // 错误提示(vant组件库) showFailToast(resData.message); } else if (resData.status === 500) { // 其他类型错误,到主页 router.push({ name: "Home", }); showFailToast(resData.message); } else { if (resData.ret && resData.data) { // 对鉴权成功返回的数据进行处理 …… } } }) .catch((err) => {}); }; getDetailInfo(); </script>
3. express 后端配置
👉 参考: (59条消息) Nodejs使用jsonwebtoken生成token和express-jwt解析和校验token_node jsonwebtoken_一只花小妖的博客-CSDN博客
-
安装需要的第三方包:jsonwebtoken(生成 jwt 字符串的包) 和 express-jwt(将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包)
-
使用 express 创建一个基本的服务器,这里我用的 8000 端口。
const express = require('express') const app = express() app.listen(8000, () => { console.log("服务已经启动,8000端口监听中……") } -
导入 jsonwebtoken 和 express-jwt 第三方包,并创建一个密钥 secretKey
// 导入jsonwebtoken生成 jwt 字符串的包 const jsonwebtoken = require('jsonwebtoken') // 将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包 const { expressjwt: jwt } = require("express-jwt") // 创建密钥 const secretKey = 'xxxxx' -
连接数据库 ⌊ mysql.createConnection(options) ⌉
const mysql = require("mysql") // 配置mysql var options = { host: 'localhost', port: 3306, user: 'root', password: 'xxxxxx', database: 'express_mysql_db' }; // 连接数据库 const connection = mysql.createConnection(options) // sql语句,注册时插入数据到表格 var add_sql = 'insert into user(`name`, `password`) values(?, ?)' -
⭐ 编写登录接口
- 查询数据库,根据电话号码判断用户是否已经注册
- 如果没有注册,则在数据库中增加用户的信息,成功后
jwt.sign({用户信息},密钥,token有效时长)生成 token, 并通过 res.send() 返回给客户端。注意: 生成 token 的用户信息不要放敏感数据(密码等) - 如果用户已经注册,进一步判断用户的密码是否正确,密码错误则返回 {status: '400',message: '密码错误'}
- 密码正确则 jwt.sign({用户信息},密钥,token有效时长) 生成 token, 并通过 res.send() 返回给客户端
app.post('/api/login', (request, response) => { let name = request.body.username let password = request.body.userPwd let userdata connection.query(`select * from user where name = ${name}`, (err, users) => { if (err) { // 查询异常 response.send('query error') } else { // 对查询到的数据库数据进行转换,方便下一步操作 userdata = JSON.parse(JSON.stringify(users)) // 判断用户是否已经注册 if (!userdata.length) { connection.query(add_sql, [name, password], (err, users) => { if (err) { console.log(err) // 查询异常 response.send('query error') } else { let tokenStr = jsonwebtoken.sign({ name }, secretKey, { expiresIn: '2h' }) response.send({ status: 200, message: '注册成功', token: tokenStr }) } }) } else { // 判断密码是否正确 if (userdata[0].password !== password) { response.send({ status: '400', message: '密码错误' }) } else { let tokenStr = jsonwebtoken.sign({ name }, secretKey, { expiresIn: '2h' }) response.send({ status: 200, message: '登录成功', token: tokenStr }) } } } }) }) -
设置中间件
- 通过
jwt({ secret: secretKey, algorithms: ["HS256"] })验证用户发过来的请求头的 token 是否有效。 unless({ path: ['/api/login', ... }))放置不需要 token 鉴权的请求路径,一定要包括 login。
// 将客户端发送过来的 jwt 字符串,解析还原成 JSON 对象的中间件 express-jwt app.use(jwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: ['/api/login', ...] })) - 通过
-
编写需要鉴权获取信息的接口,可以通过 Cache-Control 设置浏览器缓存类型(强制缓存、协商缓存)
app.get('/api/detail.json', (request, response) => { // 设置响应头 设置允许跨域 response.setHeader('Access-Control-Allow-Origin', '*') response.setHeader("Cache-Control", "max-age=600") connection.query(`select * from user where name = ${request.auth.name}`, (err, users) => { if (err) { // 查询异常 response.send('query error') } else { userdata = JSON.parse(JSON.stringify(users)) if (userdata.length) { response.send(JSON.stringify(Detail)) } else { response.send({ status: 401, message: '没有权限' }) } } }) }) -
在所有路由之后,编写错误中间件,对错误情况进行处理,结合前端部分可以对情况进行处理
app.use((err, req, res, next) => { // token 错误 if (err.name === 'UnauthorizedError') { res.send({ status: 401, message: '无效的token' }) } // 如果是其他位置原因导致的错误 res.send({ status: 500, message: '未知的错误' }) next() }) -
可以将 vue 打包后的 dist 文件夹中内容,放到服务器文件中的 public 路径,并通过 static 中间件设置静态资源目录,运行服务器时,可以通过服务器端口访问整个项目。
app.use(express.static(path.resolve(__dirname, "public"))) -
可能会出现浏览器刷新界面 401 的问题,可以使用 connect-history-api-fallback 库 解决这个问题。
const history = require('connect-history-api-fallback'); app.use('/', history())该中间件必须要放在
express.static中间件的前面引入,否则会出现问题。 -
server 文件夹结构: