文章目录
一、官方地址(中文版):
二、工程搭建
- ① 新建文件夹,打开终端,输入
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

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 测试
短时间内输错一定次数,则有验证码

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

被折叠的 条评论
为什么被折叠?



