Nodejs+Express+MySQL实现登陆功能(含验证码)

本文介绍了如何使用Nodejs、Express框架和MySQL数据库实现登录功能,包括数据库表创建、模型操作、路由处理、JWT令牌验证及验证码的生成和验证。在客户端部分,涉及Vue工程搭建、Vuex状态管理和接口封装。

一、官方地址(中文版):

https://www.expressjs.com.cn/

二、工程搭建

  • ① 新建文件夹,打开终端,输入 npm init -y
  • ② 在目录下,新建 index.js 文件

三、数据库和模型

本段可以参考:https://blog.youkuaiyun.com/weixin_45979310/article/details/124528214?spm=1001.2014.3001.5501

3.1 新建表

使用 Navicat 工具 New Database
在这里插入图片描述

3.2 数据库表连接

安装 npm i sequelize mysql2 moment
在根目录下新建 modles文件夹,新建 db.js

const { Sequelize} = require('sequelize')

module.exports = new Sequelize('nodeTest', 'root', '123456', {
    host: 'localhost',
    dialect: 'mysql'
})

在index.js中测试

const sequelize = require('./models/db')
sequelize.authenticate().then(res => {
    console.log('连接成功')
}).catch(err => {
    console.log('连接失败')
})

3.3 模型

在models目录下新建 Admin.js

const { DataTypes } = require('sequelize');
const sequelize = require('./db');
const moment = require('moment');

module.exports = sequelize.define('Admin', {
    // 第二个参数,定义模型属性(也就是列,id可以省略)
    loginId: {
        type: DataTypes.STRING, // 该列是 字符串型
        allowNull: false        // 是否可以 null 值
    },
    loginPwd: {
        type: DataTypes.STRING,
        allowNull: false
    },
    nickName: {
        type: DataTypes.STRING,
        allowNull: false
    },
    createdAt: {
    	type: DataTypes.STRING,
    	allowNull: false,
	    get() {
	        return moment.utc(this.getDataValue('createdAt')).format('YYYY-MM-DD HH:mm:ss')
	    }
	}
},{
    paranoid: true,  // 该表数据不会被真正删除,会增加一列deleteAt 用来记录删除的时间
});

同步模型:在index.js 中

require('./models/Admin');
const sequelize = require('./models/db')
sequelize.sync({alter: true}).then( () => {
    console.log('模型同步完成')
})

控制台出现打印后,在Navicat中刷新即能看到模型中的字段。

3.4 数据库的增删改查

密码使用md5加密,安装:npm install md5
在根目录下新建 services 文件夹,新建 adminService.js

const Admin = require('../models/Admin');
const md5 = require('md5');

/**
 * 添加管理员
 * @param obj Object
 */
exports.addAdmin = async function(obj) {
    obj.loginPwd = md5(obj.loginPwd);
    const ins = await Admin.create(obj);
    return ins.toJSON();
}

/**
 * 单个查询 - 登陆查询
 * @param loginId
 * @param loginPwd
 * @returns {Promise<*>}
 */
exports.login = async function(loginId, loginPwd) {
    loginPwd = md5(loginPwd);
    const res = await Admin.findOne({
        where: {
            loginId,
            loginPwd
        },
        attributes: ['id', 'loginId', 'nickName', 'createdAt']
    })
    // 如果有结果了,且大小写都相同的话,那么返回,并展开
    if(res && res.loginId === loginId && res.loginPwd === loginPwd) {
        return res.toJSON();
    }
    return null;
}

/**
 * 查询角色
 * @param id
 * @returns {Promise<null|any>}
 */
exports.getAdminById = async function(id) {
    const result = await Admin.findOne({
        where: {
            id
        },
        attributes: ['id', 'loginId', 'nickName', 'createdAt'],
    });
    if(result){
        return result.toJSON();
    }
    return null;
}

在index.js中测试

const adminService = require('./services/adminService')
adminService.addAdmin({
    loginId: 'admin',
    loginPwd: '123123',
    nickName: '超级管理员'
})
// 查询刚才新增的一条
// adminService.login('admin', '123123').then(res => {
//     console.log(res);
// })

刷新数据库表,即可看到新增的一条。

四、使用express

安装: npm install express
index.js

const express = require('express');
const app = express();  // 创建一个express应用,通常一个应用只创建一个服务器
// app 实际上是一个 用于处理请求的函数
app.listen(12306, () => {
    console.log('芜湖起飞')
})

// 处理请求
app.use('*', (req, res) => {
    res.send('哈咯阿')
})

运行 node index,在浏览器输入:http://localhost:12306/,显示 哈咯阿,说明正常。

五、使用nodemon,热更新

服务器端修改代码后,需要重新运行才能生效。
安装: npm install -D nodemon (开发依赖,所以 -D)
使用: npm nodemon index 即可
简写: package.json

  "scripts": {
    "start": "nodemon index"
  },

配置:新建 nodemon.json

{
  "ignore": ["node_modules", "dist", "client", "package*.json"]
}

以后运行 npm start 即可

六、路由处理

需要用到:cors(跨域)path-to-regexp(url正则) jsonwebtoken(token)connect-history-api-fallback(静态资源路径处理)
安装:npm install cors path-to-regexp jsonwebtoken connect-history-api-fallback

6.1 init初始化

在根目录下新建 routes文件夹,新建 init.js

const express = require("express");
const app = express();
const cors = require("cors");
const history = require('connect-history-api-fallback');

// 解决静态资源的路径刷新出错问题
app.use(history());

// 映射public目录中的静态资源
const path = require("path");
const staticRoot = path.resolve(__dirname, "../public");
app.use(express.static(staticRoot));

// 允许所有跨域
app.use(
    cors({
        origin(origin, callback) {
            if (!origin) {
                callback(null, "*");
                return;
            }
            callback(null, origin);
        },
        credentials: true,
    })
);

// 应用token中间件
app.use(require("./tokenMiddleware"));

// 解析 application/x-www-form-urlencoded 格式的请求体
app.use(express.urlencoded({ extended: true }));

// 解析 application/json 格式的请求体
app.use(express.json());

// 处理 api 的请求
app.use("/api/admin", require("./api/admin"));

// 处理错误的中间件
app.use(require("./errorMiddleware"));

const port = 12306;
app.listen(port, () => {
    console.log(`server listen on ${port}`);
});

6.2 封装消息格式

在routes文件夹下,新建 getSendResult.js

exports.getErr = function (err = "server internal error", errCode = 500) {
    return {
        code: errCode,
        msg: err,
    };
};

exports.getResult = function (result) {
    return {
        code: 0,
        msg: "",
        data: result,
    };
};

exports.asyncHandler = (handler) => {
    return async (req, res, next) => {
        try {
            const result = await handler(req, res, next);
            res.send(exports.getResult(result));
        } catch (err) {
            next(err);
        }
    };
};

6.3 处理错误中间件

在routes文件夹下,新建 errorMiddleware.js

// 处理错误的中间件
const getMsg = require("./getSendResult");
module.exports = (err, req, res, next) => {
    if (err) {
        const errObj = err instanceof Error ? err.message : err;
        //发生了错误
        res.status(500).send(getMsg.getErr(errObj));
    } else {
        next();
    }
};

6.4 admin路由

在根目录下新建 routes文件夹,新建api文件夹,新建 admin.js

const express = require("express");
const router = express.Router();
const adminServ = require("../../services/adminService");
const { asyncHandler } = require("../getSendResult");
const jwt = require('../jwt');

router.post(
    "/login",
    asyncHandler(async (req, res) => {
        const result = await adminServ.login(req.body.loginId, req.body.loginPwd);
        if (result) {
            let value = result.id;
            //登录成功
            result.token = jwt.publish(res, {id: value},undefined )
        }
        return result;
    })
);

module.exports = router;

6.5 使用jwt

在 routes 文件夹下,新建 jwt.js

const jwt = require('jsonwebtoken');
// 定义一个秘钥:用于加密和解密
const secret = 'miyao';

// 颁发 jwt
exports.publish = (res, info = {}, maxAge = 3600*24) => {
    // 第一个为payload,第二个为秘钥(用来给payload加密), 第三个为时效期
    const token = jwt.sign(info, secret, {
        expiresIn: maxAge
    })

    // 添加请求头
    res.header('authorization', token);
    return token;
}

// 校验 jwt
exports.verify = (req) => {
    let token = req.headers.authorization;

    if(!token) {
        return null;
    }

    const parts = token.split(' ');
    token = parts.length === 1 ? parts[0] : parts[1];
    try {
        const result = jwt.verify(token, secret)
        return result;
    } catch (e) {
        return null;
    }
}

6.6 token中间件

在 routes 文件夹下,新建 tokenMiddleware.js

const { getErr } = require("./getSendResult");
const { pathToRegexp } = require("path-to-regexp");
const jwt = require('./jwt');
const needTokenApi = [
    { method: "POST", path: "/api/student" },
    { method: "PUT", path: "/api/student/:id" },
    { method: "GET", path: "/api/student" },
];

// 用于解析token
module.exports = (req, res, next) => {

    const apis = needTokenApi.filter((api) => {
        const reg = pathToRegexp(api.path);
        return api.method === req.method && reg.test(req.path);
    });
    if (apis.length === 0) {
        next();
        return;
    }

    const result = jwt.verify(req);
    if (result) {
        //认证通过
        req.userId = result.id;
        next();
    } else {
        //认证失败
        handleNonToken(req, res, next);
    }
};

//处理没有认证的情况
function handleNonToken(req, res, next) {
    res
        .status(403)
        .send(getErr("you dont have any token to access the api", 403));
}

6.7 测试

将根目录下的 index.js改成

// 启动路由服务
require('./routes/init')

运行后,在 Postman 中测试,localhost:12306/api/admin/login,会有token说明成功

在这里插入图片描述

七、客服端

在根目录路径下,终端生成vue工程: vue create client
使用: Babel / Router / Vuex / CSS Pre-processors(less)

cd client
// 可以正常启动后,安装axios
npm i axios

7.1 配置文件

在client根目录下新建 vue.config.js

const { resolve } = require('path');

module.exports = {
    devServer: {
       proxy: {
           "/api": {
               target: "http://localhost:12306"
           }
       }
    },
    // 打包到上一层目录的public
    outputDir: resolve(__dirname, "../public"),
}

7.2 服务接口

在src 目录下,新建 service文件夹,

7.2.1 封装接口

新建 request.js

import axios from 'axios'

export default () => {
    const token = localStorage.getItem('token');
    let instance = axios;
    // 如果没有token,那么instance就是普通的axios,
    // 如果有,按照我指定的格式(添加到响应头中,拼接bearer)
    if (token) {
        instance = axios.create({
            headers: {
                authorization: "bearer " + token
            }
        })
    }

    instance.interceptors.response.use((resp) => {
        if (resp.headers.authorization) {
            localStorage.setItem("token", resp.headers.authorization)
        }
        return resp;
    }, (err) => {
        console.log(err)
        if (err.response.status === 403) {
            localStorage.removeItem("token");
        }
        return Promise.reject(err);
    })

    return instance;
}
7.2.2 登陆接口

新建 loginservice.js

import request from './request';

export const login = async (loginId, loginPwd) => {
    const resp = await request().post("/api/admin/login", {loginId, loginPwd})
    return resp.data;
}

export const loginOut = () => {
    localStorage.removeItem('token')
}

export const whoAmI = async () => {
    try {
        await delay(2000);	// 模拟真实接口
        const resp = await request().get('/api/admin/whoAmI');
        return resp.data;
    }catch (err) {
        console.log(err)
    }
}

/**
 * 延时函数
 * @param time
 * @returns {Promise<unknown>}
 */
const delay = (time) => (new Promise(resolve => {
    setTimeout(() => {
        resolve();
    }, time)
}))

7.3 使用vuex

  • 在store文件夹下的index.js 中使用modules
import Vue from 'vue'
import Vuex from 'vuex'
import loginUser from './login'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    loginUser
  }
})
  • 新建login.js
import * as request from '../service/loginservice'

export default {
    namespaced: true,
    state: {
        data: null,
        isLoading: false
    },

    mutations: {
        setData(state, payload) {
            state.data = payload;
        },
        setIsLoading(state, payload) {
            state.isLoading = payload;
        }
    },
    actions: {
        async login({commit}, {loginId, loginPwd}) {
            commit('setIsLoading', true);
            try {
                const {data} = await request.login(loginId, loginPwd)
                commit('setData', data);
                commit('setIsLoading', false);
                return data;
            } catch (e) {
                console.log('没有token', e)
                commit('setData', null);
                return null;
            }
        },
        loginOut({commit}) {
            commit('setData', null);
            request.loginOut();
        },
        async whoAmI({ commit }) {
            commit("setIsLoading", true);
            try {
                const resp = await request.whoAmI();
                commit("setData", resp.data);
            } catch {
                commit("setData", null);
            }
            commit("setIsLoading", false);
        },
    }
}
  • 在根目录的 main.js 中使用
// 在网站被访问时,需要用token去换取用户的身份
store.dispatch("loginUser/whoAmI");

7.4.1 页面

  • HomeView.vue
<template>
  <div class="home">
    大家都可以看
  </div>
</template>
  • ProtectView.vue
<template>
  <div class="about">
    <h1>需要登录,才能看哦</h1>
  </div>
</template>
  • LoginView.vue
<template>
  <div>
    <p>账号<input type="text" v-model="loginId" autocomplete="new-password"></p>
    <p>密码<input type="password" v-model="loginPwd" autocomplete="new-password"></p>
    <button @click="submit">立即登录</button>
  </div>
</template>

<script>
import * as request from '../service/loginservice'
export default {
  name: "LoginView",
  data() {
    return {
      loginId: '',
      loginPwd: ''
    }
  },
  methods: {
    async submit() {
      const res = await this.$store.dispatch('loginUser/login', {
        loginId: this.loginId,
        loginPwd: this.loginPwd
      })
      if(res) {
        this.$router.push("/");
      } else {
        window.confirm('密码错误')
      }
    }
  }
}
</script>
  • App.vue
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/protect">Protect</router-link> |
      <a v-if="isLoading">Loading...</a>
      <template v-else-if="data">
        <a>{{ data.loginId }}</a>
        <button @click="loginOut">注销</button>
      </template>
      <router-link v-else to="/login">Login</router-link>
    </nav>
    <router-view/>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: mapState("loginUser", ["data", "isLoading"]),
  methods: {
    loginOut() {
      this.$store.dispatch("loginUser/loginOut");
      this.$router.push("/login");
    },
  },
}
</script>

<style lang="less">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>

7.5 路由

在 router 下的 index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import store from "../store";

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/protect',
    name: 'protect',
    component: () => import(/* webpackChunkName: "protect" */ '../views/ProtectView.vue'),
    beforeEnter(to, from, next) {
      if (store.state.loginUser.data) {
        //有用户
        next();
      } else {
        next("/login");
      }
    },
  },
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: "login" */ '../views/LoginView.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

7.6 测试

分别运行客户端和服务端,输入账号密码,可以登陆,刷新后可以点Protect页面
在这里插入图片描述

八、验证码

8.1 服务端

使用 svg-captcha 插件,安装 npm i svg-captcha

  • 在routes文件下,新建 captchaMid.js
// 生成验证码
const express = require("express");
const router = express.Router();
const svgCaptcha = require("svg-captcha");

router.get('/api/captcha', (req, res) => {
    const {text, data} = svgCaptcha.create();
    // 将验证码中的文本存到服务器的session中
    req.session.captcha = text.toLowerCase();

    // 将验证码中的图片发送给客户端
    res.type('svg');
    res.status(200).send(data);
})

const validateCaptcha = (req, res, next) => {
    const reqCaptcha = req.body.captcha ? req.body.captcha.toLowerCase() : ""; //用户传递的验证码
    if (reqCaptcha !== req.session.captcha || reqCaptcha == '') {
        //验证码有问题
        res.send({
            code: 401,
            msg: "验证码有问题",
        });
    } else {
        next();
    }

    req.session.captcha = "";
}

const captchaHandler = (req, res, next) => {
    // 如果session中没有访问记录
    if(!req.session.records) {
        req.session.records = [];
    }
    const now = new Date().getTime();
    req.session.records.push(now);  // 记录请求时间

    // 如果在一小段时间中请求达到了一定的数量,就可能是机器
    const duration = 10000;
    const repeat = 3;

    req.session.records = req.session.records.filter((time) => now - time <= duration);

    if (req.session.records.length >= repeat || "captcha" in req.body) {
        // 验证验证码
        validateCaptcha(req, res, next);
    } else {
        next();
    }
}

router.post("*", captchaHandler);
router.put("*", captchaHandler);

module.exports = router;
  • init.js 中,引入中间件
// 生成验证码的中间件
app.use(require("./captchaMid"));

8.2 客户端

  • 修改 LoginView.vue 文件
<template>
  <div>
    <p>账号<input type="text" v-model="loginId" autocomplete="new-password"></p>
    <p>密码<input type="password" v-model="loginPwd" autocomplete="new-password"></p>
    <div id="captchaArea" v-if="isShow">
      验证码:<input type="text" v-model="captcha" />
      <span v-html="imgUrl" @click="getImg"></span>
    </div>
    <button @click="submit">立即登录</button>
  </div>
</template>

<script>
import * as request from '../service/loginservice'
export default {
  name: "LoginView",
  data() {
    return {
      loginId: '',
      loginPwd: '',
      imgUrl: '',
      isShow: false,
      captcha: ''
    }
  },
  methods: {
    async getImg() {
      this.imgUrl = await request.getImg();
    },
    async submit() {
      let params = {
        loginId: this.loginId,
        loginPwd: this.loginPwd
      }
      if(this.isShow) {
        params.captcha = this.captcha;
      }
      const {code, data} = await this.$store.dispatch('loginUser/login', params)
      // 验证码功能
      if (code === 401) {
        this.isShow = true;
        this.getImg();
        this.refreshCaptcha();
      } else if (data) {
        this.$router.push("/");
      } else {
        window.alert('密码错误')
        if(this.isShow) this.getImg();
      }
    },
  }
}
</script>

  • LoginService.js/store/login.js 中login参数,增加一个 captcha
// LoginService.js
{loginId, loginPwd, captcha}

// /store/login.js
async login({commit}, {loginId, loginPwd, captcha}) {
    commit('setIsLoading', true);
    try {
        const {code, data} = await request.login(loginId, loginPwd, captcha)
        commit('setData', data);
        commit('setIsLoading', false);
        return {code, data};
    } catch (e) {
        console.log('没有token', e)
        commit('setData', null);
        return null;
    }
},

8.3 测试

短时间内输错一定次数,则有验证码
在这里插入图片描述

== 结束==

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值