express+mysql搭建后台(一)

153 阅读3分钟

一、安装依赖

基础

npm i express
const app = express();

解析post请求主题

npm i body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.json());

解析cookie

npm i cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());

使用session

npm i express-session
const session = require('express-session');
app.use(session({
  secret: 'my secret',
  resave: false,  // 重新保存
  saveUninitialized: true  // 自动保存
}));

token加密解析

npm i jsonwebtoken
const jwt = require('jsonwebtoken')

日志

npm i express-winston
const {globalLogger}=require('./winstonLogger.js')
app.use(expressWinston.logger({
  winstonInstance: globalLogger,
  meta: true, // 如果设置为 true,则会在日志中添加一些元数据,例如请求的 URL、方法、HTTP 版本等
  msg: "HTTP {{req.method}} {{req.url}}", // 日志消息的格式。你可以使用双大括号 {{}} 来引用请求或响应的属性。
  expressFormat: true, // 如果设置为 true,则日志消息的格式会类似于 Express 的默认日志格式 :method :url :status :response-time ms - :res[content-length]。
  colorize: true, // 如果设置为 true,则日志消息会被着色
  requestWhitelist: ['url', 'headers', 'method', 'httpVersion', 'originalUrl', 'query','body','ip'],// 一个数组,用于指定哪些请求属性应该被记录
  ignoreRoute: function (req, res) { return false; } // 
}));
// winstonLogger.js
const winston = require("winston");
const DailyRotateFile =require("winston-daily-rotate-file");
const customFormat = winston.format.combine(
    winston.format.timestamp({ format: "MMM-DD-YYYY HH:mm:ss" }),
    winston.format.align(),
    winston.format.printf(({level,timestamp,message,meta}) =>{
        return `${level}:
        Time:${[timestamp]}
        Url:${meta.req.url}
        Method:${meta.req.method}
        headers:${JSON.stringify(meta.req.headers)}
        httpVersion:${meta.req.httpVersion}
        originalUrl:${meta.req.originalUrl}
        query:${JSON.stringify(meta.req.query)}
        Message:${message}
        body:${JSON.stringify(meta.req.body)}
        ip:${meta.req.ip}`
    })
);
const defaultOptions = {
    format: customFormat,
    datePattern: "YYYY-MM-DD",
    zippedArchive: true,
    maxSize: "20m",
    maxFiles: "14d",
};
const globalLogger = winston.createLogger({
    format: customFormat,
    transports: [
        new DailyRotateFile({
            filename: "logs/info-%DATE%.log",
            level: "info",
            ...defaultOptions,
        }),
        new DailyRotateFile({
            filename: "logs/error-%DATE%.log",
            level: "error",
            ...defaultOptions,
        }),
    ],
});

const authLogger = winston.createLogger({
    transports: [
        new DailyRotateFile({
            filename: "logs/authLog-%DATE%.log",
            ...defaultOptions,
        }),
    ],
});

module.exports = {
    globalLogger: globalLogger,
    authLogger: authLogger,
};

密码加密(单向)

npm i crypto
// util.js
const crypto = require('crypto');
const getHashValue = (value, salt = '') => {
    // 生成一个随机的盐值
    if (!salt) {
        salt = crypto.randomBytes(16).toString('hex');
    }
    // 把盐值添加到密码中,然后进行哈希
    const hashValue = crypto.pbkdf2Sync(value, salt, 1000, 64, 'sha512').toString('hex');
    const saltedHash = salt + ":" + hashValue;
    return saltedHash
}

验证函数

npm i express-validator
const { body, validationResult,check } = require('express-validator');
// 封装验证函数
const validateFields = (fields) => {
    const validators = [];
    fields.forEach(({ field, noEmpty, validType, errorMessage }) => {
        let validator = body(field);
        if (noEmpty) {
            validator = validator.notEmpty().withMessage(errorMessage || `${field} must not be empty`);
        }
        if(!noEmpty&&validator.isEmpty()){
            validator = check('fieldName')
            .custom(value => {
              if (value === null || value === undefined || value === '') {
                return true; // 验证通过
              }
              return true; // 如果字段不为空,也返回 true
            })
            .withMessage('The field cannot be empty');
        }
        else if (validType === 'string') {
            validator = validator.isString().withMessage(errorMessage || `${field} must be a string`);
        } else if (validType === 'int') {
            validator = validator.isInt().withMessage(errorMessage || `${field} must be an integer`);
        } else if (validType === 'boolean') {
            validator = validator.isBoolean().withMessage(errorMessage || `${field} must be a boolean`);
        } else if (validType === 'email') {
            validator = validator.isEmail().withMessage(errorMessage || `${field} must be a valid email`);
        }
        validators.push(validator);
    });

    validators.push((req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        next();
    });

    return validators;
}

数据库

npm i mysql
// mysql.js
const mysql = require('mysql');
// 数据库连接配置
const pool = mysql.createPool({
    connectionLimit : 10,
    host     : 'localhost',
    user     : 'root',
    password : '123456',
    database : 'mydb',
    multipleStatements:true,
});
pool.on('enqueue', function (sequence) {
    if ('Query' === sequence.constructor.name) {
        console.log('enqueue',sequence.sql);
    }
});
pool.on('connection', function (conn) { 
    conn.on('enqueue', function(sequence) {
        if ('Query' === sequence.constructor.name) {
            console.log('connection',sequence.sql);
        }
    });
});
module.exports=pool

二、监听

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

三、获取token并拦截

使用中间件的方式

const noNeedToken=['/Admin/Login']
app.use((req, res, next) => {
  if(noNeedToken.includes(req.originalUrl)){
    next()
  }else{
    const token = req.headers['authorization']||req.headers['token'];
    if (!token) {
      return res.status(401).send(errorMsg('无法获取token'));
    }
    try {
      const decoded = jwt.verify(token, security_key,{maxAge:'7d',complete:true});
      // security_key为自己定义的密钥
      next(); // 继续执行后续中间件或路由处理器
    } catch(err) {
      if(err.name === 'TokenExpiredError') {
        return res.status(401).send(errorMsg('token已过期'));
      } else {
        return res.status(401).send(errorMsg('不合法的token'));
      }
    }
  }
});

四、常量

将数据库的下划线字段转为驼峰

npm i humps
const humps = require('humps');
/**
 * @param {any | undefined} [data]
 */
function successMsg(data) {
    return {
        code: 0,
        data: humps.camelizeKeys(data),
        msg: '成功'
    }
}

/**
 * @param {any | undefined} [data]
 */
function errorMsg(data) {
    return {
        code: -1,
        data: humps.camelizeKeys(data),
        msg: '失败'
    }
}
function page({data,total,pageIndex}){
    return {
        records:data,
        total,
        pageIndex
    }
}
const security_key='test'
module.exports = {
    successMsg,
    errorMsg,
    page,
    security_key
}

五、开始配置接口

以登录接口为例

安装multer

Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据

npm i multer
const pool = require('../mysql/mysql.js');
const express = require('express');
const router = express.Router();
const multer = require('multer');
const upload = multer();
const jwt = require('jsonwebtoken')
const { getHashValue, validateFields } = require('../public/util.js')
const { successMsg, errorMsg,page, security_key } = require('../public/constant.js')
router.post('/Login', upload.none(), validateFields([
  { field: 'userName', noEmpty: true, validType: 'string' },
  { field: 'password', noEmpty: true, validType: 'string' }
]), (req, res, next) => {
  const { userName, password } = req.body;
  // 参数1: 生成到token中的信息
  // 参数2: 密钥
  // 参数3: token的有效时间: 60, "2 days", "10h", "7d"
  const sql = `SELECT * FROM ${tableName} WHERE user_name=? and is_delete=0`
  pool.query(sql, [userName], (err, result) => {
    if (err){
      return res.status(500).send(errorMsg('数据库错误'));
    }
    // 将解密后的password(buffer)转为字符串
    var r = [...result]
    if (r.length) {
      const [storedSalt, storedHash] = r[0].password.split(":");
      const hashedPassword = getHashValue(password, storedSalt)
      if (hashedPassword == r[0].password) {
        const token = jwt.sign({ userName }, security_key, { expiresIn: '7d' })
        res.status(200).send(successMsg(token));
      } else {
        res.status(200).send(errorMsg('密码错误'))
      }
    } else {
      res.status(200).send(errorMsg('没有该用户'))
    }

  });
});
// 最后导出
module.exports = router;

六、使用接口

在监听之前

const adminRouter = require('./api/admin.js');
app.use('/Admin', adminRouter);
// 接下来便可调用/Admin/Login进行测试了

下篇为swagger的使用,期待!!!