初始化后端
mkdir beetle-master
cd beetle-master
运行
npm init,按照提示填写信息,初始化项目。这将生成一个package.json文件,用于管理项目依赖和配置。
安装依赖
npm install body-parser express jsonwebtoken mysql2 nodemailer sequelize
- express: Web应用框架
- mysql2: 适用于Node.js的MySQL客户端,专注于性能优化。
- sequelize:是一个易于使用且基于承诺的Node.js ORM 工具,适用于Postgres、MySQL、MariaDB、SQLite、DB2、Microsoft SQL Server和Snowflake
- nodemailer: Nodemailer是一个简单易用的Node.js邮件发送组件
- body-parser: 非常常用的一个express 中间件,作用是对post请求的请求体进行解析
- jsonwebtoken 生成和验证JSON Web Token(JWT)
搭建服务器
现在我们要创建文件夹config/config.js配置文件
/**
* @description 服务器配置
*/
exports.serverOptions = {
host: "http://127.0.0.1", // 本地开发
port: 10001, // 端口 10001
// host: '服务器地址', // 阿里云服务器
// port: 3000, // 端口 3000
baseUrl: "/static/file/", // 伪造路径
};
**
* @description 随机昵称
*/
exports.nickNameOptions = [
"彩虹",
"白云",
"森林",
"蓝天",
"大海",
"领悟",
"实诚",
"飞鸟",
"老鹰",
"白兔",
"绿竹",
];
/**
* @description 加盐配置, 用于加强加密
*/
exports.saltOptions = {
pwd: "loser", // 密码加盐
};
/**
* @description 数据库配置
*/
exports.mysqlOptions = {
database: "server_db", // 数据库名称
username: "root", // 用户名
password: "root", // 登录密码
host: "localhost", // 数据库地址
dialect: "mysql", // 数据库类型
timezone: "+08:00", // 时区
underscored: true, // 字段以_命名
forceRemove: false, // 是否强制删除表
};
/**
* @description 邮件配置, 验证码
*/
exports.emailOptions = {
host: "smtp.qq.com", // 邮件服务器地址
port: 465, // 端口, 25端口在阿里云服务器被禁止的, 建议使用465
secure: true, // 如果端口为465, 此项需要设置true, 其他端口需要修改为false
user: "xxxx@qq.com", // 用户名,发件地址
pass: "xxxxxx", // 邮箱授权码
expires: 5 * 60 * 1000, // 验证码有效时间, 单位:毫秒
};
/**
* @description 允许请求(白名单)
*/
exports.hostOptions = [
"http://127.0.0.1:3000",
"http://127.0.0.1:8080",
"http://127.0.0.1:8081",
"http://127.0.0.1:8082",
"http://127.0.0.1:9000",
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:9000",
];
/**
* @description 验证验证码请求路径
*/
exports.codeUrlOptions = ["/register"];
/**
* @description token配置
*/
exports.tokenOptions = {
salt: "_t_k", // token加盐
expires: "1d", // 有效时间
tokenUrls: [
"/getUserInfo",
"/postProduct",
"/search",
"/count",
"/updown",
"/probyid",
"/updateProduct",
"/remove",
"/like",
"/removeLike",
"/findLike",
"/likeCount",
"/addShopcart",
"/removeShopcart",
"/shopcartProducts",
"/shopcartCount",
], // 需要验证token的请求路径
};
/**
* @description 文件存放配置
*/
exports.filePathOptions = {
falsePath: "/static/file", // 伪路径
imgPath: "/upload", // 图片存储路径
};
现在创建db/connct.js 连接数据库
// 导入sequelize, 并且解构Sequelize
let { Sequelize } = require("sequelize");
// 连接MySQL数据库 config是全局变量
let sequelize = new Sequelize(
config.mysqlOptions.database,
config.mysqlOptions.username,
config.mysqlOptions.password,
{
// 数据库地址
host: config.mysqlOptions.host,
// 数据库类型
dialect: config.mysqlOptions.dialect,
// 时区
timezone: config.mysqlOptions.timezone,
// 字段命名 以 _ 分隔
define: {
underscored: config.mysqlOptions.underscored,
},
}
);
// 导出实例,以便后续创建模型需要使用该连接实例
module.exports = sequelize;
现在创建db/model/model.js文件进行导入数据模型
/**
* @description 导出所有模型
*/
module.exports = {};
创建routes_controller.js路由控制器
// 导入sequelize的Op模块
let { Op } = require("sequelize");
/**
* @description holle word
* @param {object} req 请求体
* @param {object} res 响应体
*/
home(req, res) {
res.send("hello word")
}
}
// 导出实例
module.exports = new RoutesController();
创建router/router.js文件
// 导入路由控制器
let routesController = require(__basename +
"/routes_controller/routes_controller.js");
// 导出路由函数
module.exports = (app) => {
//首页
app.post("/", routesController.home);
};
创建一个名为index.js的文件并写入代码
// global: nodejs全局变量对象
global.__basename = __dirname;
// 导入express
let express = require("express");
// 导入body-parser 中间件
let bodyParser = require("body-parser")
// 导入config.js
global.config = require(__basename + "/config/config.js");
// 导出connect.js, 连接mysql数据库
global.sequelize = require(__basename + "/db/connect.js");
// 导入model.js ==> 所有模型, 全局挂载model,以便后面操作数据
global.model = require(__basename + "/db/model/model.js");
// 导入路由
let routes = require(__basename + "/routes/routes.js");
// 创建实例
let app = express();
// 解析post请求体
// 将post请求参数解析为json
// limit限制解析post请求体大小 5MB
app.use(
bodyParser.json({
limit: 5 * 1024 + "kb",
})
);
// extended:false接收任何数据格式, true接收字符串或者数组
app.use(
bodyParser.urlencoded({
extended: false,
limit: 5 * 1024 + "kb",
})
);
/ 设置CORS, 跨域处理
// CORS 跨域资源共享
// app.all(*)表示所有请求路径必须经过
app.all("*", (req, res, next) => {
// 允许跨域地址
// res.header('Access-Control-Allow-Origin', '跨域地址')
// *表示允许所有域请求, 但是不允许跨域携带cookie, 在实际开发中,一般指定允许某个域请求,如上面设置
// res.header('Access-Control-Allow-Origin', '*')
// 动态允许域名请求,这样可以允许跨域携带cookie
res.header("Access-Control-Allow-Origin", req.headers.origin);
// 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
res.header("Access-Control-Allow-Headers", "X-Requested-With,token");
// 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可
res.header("Access-Control-Allow-Credentials", true);
// 将请求传递给下一个中间件或者路由
next();
});
// 加载所有路由
routes(app);
// 监听端口
app.listen(config.serverOptions.port, () => {});
设置白名单
在router.js里面设置
// 请求域拦截(白名单)
app.use(routesController.verfiyHost);
// 验证码拦截(白名单)
app.use(routesController.verifyCode);
// 验证登录(白名单)
app.use(routesController.verifyToken);
接下来我们到路由控制器里面写verfiyHost,verifyCode,verifyToken三个方法
实现verifyCode方法
/**
* @description 请求域拦截
* @param {object} req 请求体
* @param {object} res 返回体
* @param {function} next 下一步
*/
verfiyHost(req, res, next) {
if (config.hostOptions.indexOf(req.headers.origin) === -1) {
return res.send({ msg: "请求域不合法", status: 1020 });
}
next();
}
实现verifyCode方法
创建code验证码实体
let { DataTypes, Model } = require("sequelize");
class Code extends Model {}
/**
* @description 定义模型结构, 数据表结构
*/
Code.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
allowNull: false,
comment: "表id",
},
codeId: {
type: DataTypes.STRING(30),
allowNull: false,
// 默认值
defaultValue: "",
comment: "验证码id",
},
code: {
type: DataTypes.STRING(6),
allowNull: false,
defaultValue: "",
comment: "验证码",
},
email: {
type: DataTypes.STRING(40),
allowNull: false,
defaultValue: "",
comment: "邮箱",
},
},
{
// 指定连接实例,这样才知道在指定数据库创建code表
sequelize,
// 模型名称, 当没有指定表名时,sequelize推断名称为模型名称的复数 ==> codes作为表名
modelName: "code",
// 不推断,直接使用模型作为表名 ==> code作为表名
freezeTableName: true,
// 指定表名
tableName: "code",
}
);
// 同步数据库表
// force: true ==> 删除原有code表,新建code表
// force: false ==> 如果code存在,则不创建,反之,不创建code表
Code.sync({ force: config.mysqlOptions.forceRemove });
module.exports = Code;
在model.js中导入code实体
// 导入验证码模型
let Code = require(__basename + "/db/model/code.js");
/**
* @description 导出所有模型
*/
module.exports = {
Code,
};
创建api.js
// 导入sequelize模块
const { QueryTypes } = require("sequelize");
/**
* @description 数据库API接口
* @class API
*/
class API {
/**
* @description 查询数据库表数据
* @param {{ modelName: string, condition: object, attributes: array }} o o.modelName: 模型名称, * o.condition: 查询条件, o.attributes: 查询字段 ['a', 'b'] 或者 具有别名 ['a', ['b', 'b别名']]
* @returns {object}
*/
findData(o) {
return model[o.modelName].findAll({
where: o.condition,
attributes: o.attributes,
});
}
}
module.exports = new API();
在路由控制器里面实现
/**
* @description 验证码拦截
* @param {object} req 请求体
* @param {object} res 返回体
* @param {function} next 下一步
*/
verifyCode(req, res, next) {
// 验证验证码
let url = req.url.split("?")[0];
if (config.codeUrlOptions.includes(url)) {
api
.findData({
modelName: "Code",
condition: {
codeId: req.body.codeId,
},
})
.then((result) => {
// 获取当前时间和验证码有效时间差
let time = new Date().getTime() - config.emailOptions.expires;
// 获取验证码保存时间
let codeTime = new Date(result[0].dataValues.createdAt).getTime();
// 如果验证码保存时间 >= time
let isPass =
req.body.validcode == result[0].dataValues.code &&
req.body.email == result[0].dataValues.email &&
codeTime >= time;
// 如果验证通过,则将请求传递给下一个中间件或者路由
isPass ? next() : res.send({ msg: "验证码错误", status: 1031 });
})
.catch((err) => {
console.log("verifyCode err ==>", err);
res.send({ msg: "验证码错误", status: 1031 });
});
} else {
next();
}
}
实现verifyToken方法
创建utils.js方法
// 导入crypto, 用于加密字符串, nodejs核心模块
let crypto = require("crypto");
// 导入jsonwebtoken, 用于登录验证
let jsonwebtoken = require("jsonwebtoken");
// 导入文件系统模块, nodejs核心模块
let fs = require("fs");
// 导入nodemailer, 用于发邮件
let nodemailer = require("nodemailer");
// 发邮件配置
// 创建发邮件实例
let transporter = nodemailer.createTransport({
// 服务器地址
host: config.emailOptions.host,
// 端口, 25端口在阿里云服务器被禁止的, 建议使用465,
port: config.emailOptions.port,
// 如果端口为465,此项需要设置true, 其他端口需要修改为false
secure: config.emailOptions.secure,
// 授权认证
auth: {
// 用户邮箱地址
user: config.emailOptions.user,
// 授权码(不是邮箱登录密码)
pass: config.emailOptions.pass,
},
});
/**
* @description 公共方法
* @class Utils
*/
class Utils {
/**
* @description 解析token
* @param {string} token
*/
verifyToken(token) {
return new Promise((resolve, reject) => {
jsonwebtoken.verify(token, config.tokenOptions.salt, (err, info) => {
// 如果验证失败
if (err) {
reject(err);
} else {
resolve(info);
}
});
});
}
/**
* @description 将cookie字符串转成对象
* @param {string} cookie cookie字符串
*/
transformCookie(cookie) {
// 按照; 切割
let cookieObj = {};
let cookies = cookie.split("; ");
cookies.map((v) => {
let c = v.split("=");
cookieObj[c[0]] = c[1];
});
return cookieObj;
}
}
module.exports = new Utils();
在路由控制器里面导入utils.js
// 导入utils, 调用公共方法
let utils = require(__basename + "/utils/utils.js");
/**
* @description 验证Token(验证登录)
* @param {object} req 请求体
* @param {object} res 响应体
* @param {function} next 下一步
*/
verifyToken(req, res, next) {
// 需要验证token
let url = req.url.split("?")[0];
if (config.tokenOptions.tokenUrls.indexOf(url) > -1) {
if (!req.headers.token) {
res.send({ msg: "请先登录", status: 1034 });
} else {
let cookie = utils.transformCookie(req.headers.token);
let token = [cookie.mama12, cookie.nana20, cookie.mama20].join(".");
// 验证token
utils
.verifyToken(token)
.then((result) => {
// 将userId传递
req.userId = result.data;
next();
})
.catch((err) => {
console.log("verifyToken err ==>", err);
res.send({ msg: "请先登录", status: 1034 });
});
}
} else {
next();
}
}