[ 登录注册的前后端实现(vue + express + Mysql )| 青训营笔记 ]

310 阅读4分钟

登录注册的前后端实现(vue + express + Mysql )

1. 安装 Mysql 并创建一个保存用户数据的表

  1. 安装 Mysql: (59条消息) mysql数据库安装(详细)mysql安装体会!的博客-CSDN博客

  2. 安装 navicat: Navicat Premium 16 永久破解激活 - 酷酷的阿杰 - 博客园 (cnblogs.com)

  3. 启动 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';
    
  4. 创建数组库、数据表、填充基本数据:

    新建 express_mysql_db.sql 文件,统一创建数据库 express_mysql_db、数据表 user,并填充基本数据

    # 创建空数据库
    DROP DATABASE IF EXISTS express_mysql_db;
    CREATE DATABASE express_mysql_db;
    ​
    USE express_mysql_db;
    ​
    # 创建 userCREATE 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');
    
  5. 导入数据: 使用 .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博客

  1. 在项目中使用 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) {
              }
          }
      }
      
  2. 在登录页面 (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, "&lt").replace(/>/g, "&gt");
      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("退出成功");
    };
    
  3. 在 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 } //登录成功后跳入浏览的当前页面
                        })
                }
            }
        })
    
  4. 需要鉴权才能发送请求的组件,需要结合后端的响应对一些情况进行处理

    <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博客

  1. 安装需要的第三方包:jsonwebtoken(生成 jwt 字符串的包) 和 express-jwt(将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包)

  2. 使用 express 创建一个基本的服务器,这里我用的 8000 端口。

    const express = require('express')
    const app = express()
    app.listen(8000, () => {
        console.log("服务已经启动,8000端口监听中……")
    }
    
  3. 导入 jsonwebtoken 和 express-jwt 第三方包,并创建一个密钥 secretKey

    // 导入jsonwebtoken生成 jwt 字符串的包
    const jsonwebtoken = require('jsonwebtoken')
    // 将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包
    const { expressjwt: jwt } = require("express-jwt")
    // 创建密钥
    const secretKey = 'xxxxx'
    
  4. 连接数据库 ⌊ 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(?, ?)'
    
  5. 编写登录接口

    • 查询数据库,根据电话号码判断用户是否已经注册
    • 如果没有注册,则在数据库中增加用户的信息,成功后 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
                        })
                    }
                }
            }
        })
    })
    
  6. 设置中间件

    • 通过 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', ...] }))
    
  7. 编写需要鉴权获取信息的接口,可以通过 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: '没有权限'
                    })
                }
            }
        })
    })
    
  8. 在所有路由之后,编写错误中间件,对错误情况进行处理,结合前端部分可以对情况进行处理

    app.use((err, req, res, next) => {
    	// token 错误
        if (err.name === 'UnauthorizedError') {
            res.send({
                status: 401,
                message: '无效的token'
            })
        }
        // 如果是其他位置原因导致的错误
        res.send({
            status: 500,
            message: '未知的错误'
        })
        next()
    })
    
  9. 可以将 vue 打包后的 dist 文件夹中内容,放到服务器文件中的 public 路径,并通过 static 中间件设置静态资源目录,运行服务器时,可以通过服务器端口访问整个项目。

    app.use(express.static(path.resolve(__dirname, "public")))
    
  10. 可能会出现浏览器刷新界面 401 的问题,可以使用 connect-history-api-fallback 库 解决这个问题。

    const history = require('connect-history-api-fallback');
    app.use('/', history())
    

    该中间件必须要放在 express.static中间件的前面引入,否则会出现问题。

  11. server 文件夹结构:

    image.png