博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于 express + mysql + redis 搭建多用户博客系统
阅读量:5961 次
发布时间:2019-06-19

本文共 11588 字,大约阅读时间需要 38 分钟。

1. 项目地址

2. 项目实现

  • Express 框架
    • Node 连接 MySQL
    • 路由处理
    • API 接口开发
    • 开发中间件
  • 登录
    • Cookie / Session 机制
    • 登录验证中间件开发
    • 使用 Redis 存储 Session
  • 数据存储
    • MySQL
    • Redis
  • 安全防御
    • SQL 注入
    • XSS 攻击
  • Nginx 反向代理

  • 日志操作
    • stream 流
    • morgan 处理日志
    • crontab 日志拆分,任务定时
    • readline 逐行分析日志
  • 线上环境部署
    • 使用 PM2
    • 进程守护,系统崩溃自启动
    • 启动多进程
    • 线上日志记录

3. 项目依赖

使用 express-generator 初始化项目

跨平台环境变量设置:

$ npm install cross-env --save-dev

安装文件监测工具 nodemon:

$ npm install nodemon --save-dev
"dependencies": {    "connect-redis": "^3.4.1",    "cookie-parser": "~1.4.4",    "debug": "~2.6.9",    "express": "~4.16.1",    "express-session": "^1.16.1",    "http-errors": "~1.6.3",    "jade": "~1.11.0",    "morgan": "~1.9.1",     "mysql": "^2.17.1",    "redis": "^2.8.0",      "xss": "^1.0.6"    },  "devDependencies": {    "cross-env": "^5.2.0", // 跨平台环境变量设置    "nodemon": "^1.19.1"   // 开发环境下,文件监测  }

启动项目:

$ npm run dev

4. 文件目录

├── README.md├── project.json                    // 项目配置文件├── app.js                          // 项目主文件├── bin│   └── www                         // 项目启动入口├── conf│   └── db.js                       // mysql和redis配置文件(开发环境和线上环境)│── controller                      // 数据层│   ├── blog.js                     // 处理blog数据的增删改查│   └── user.js                     // 处理user数据, 登录│── db                              // 数据层│   ├── mysql.js                    // mysql连接,promise 统一处理sql语句│   └── redis.js                    // redis连接│── middleware                      // 存放中间件的目录│   └── loginCheckt.js              // 登录校验的中间件 │── logs                            // 存放日志的目录│   │── access.log                  // 访问日志 │   │── error.log                   // 错误日志 │   └── event.log                   // 事件日志 │── model                           // 存放中间件的目录│   └── resModel.js                 // 统一定义各个接口返回的数据格式 │── public                          // 存放前端静态文件的目录(对于前后端分类的项目不需要)│── views                           // 前端视图文件目录,对于前后端分离项目,不需要 │── routes                          // 路由层│   ├── blog.js                     // blog 操作 接口│   └── user.js                     // user 登录 接口└── utils                           // 存放中间件的目录   └── cryp.js                      // cypto 加密处理

5. Mysql 和 Redis 数据库

环境变量配置

项目从开发、测试、预发布到生成环境(线上)的环境变量一般都是不同的,为避免每次都手动修改,这里先配置环境变量

/conf/db.js:

const env = process.env.NODE_ENV // 环境参数// 配置let MYSQL_CONF let REDIS_CONF// 开发环境下if (env === 'dev') {    // mysql 配置    MYSQL_CONF = {        host: 'localhost',        user: 'user',        password: 'password',        port: '3306',        database: 'database'    }    // redis 配置    REDIS_CONF = {        host: '127.0.0.1',        port: 6379    }// 线上环境时,这里和开发环境配置一样,当发布到线上时,需要将配置改为线上if (env === 'production') {    MYSQL_CONF = {        host: 'localhost',        user: 'user',        password: 'password',        port: '3306',        database: 'database'    }    REDIS_CONF = {        host: '127.0.0.1',        port: 6379    }}// 其他环境配置... ...module.exports = {    MYSQL_CONF,    REDIS_CONF,}

MySQL 连接与使用

/db/mysql.js:

let mysql = require('mysql')const { MYSQL_CONF } = require('../conf/db')let connection = mysql.createConnection(MYSQL_CONF)connection.connect((err, result) => {    if (err) {        console.log("数据库连接失败");        return;    }    console.log("数据库连接成功");})// 通过 Promise 统一执行 sql 函数function exec(sql) {    return new Promise((resolve, reject) => {        connection.query(sql, (err, result) => {            if (err) {                reject(err)                return;            }            resolve(result)        })    })}module.exports = {    exec,    escape: mysql.escape}

例如:根据 id 查询:

const getDetail = (id) => {    const sql = `select * from blogs where id='${id}';`    return exec(sql).then(rows => {        return rows[0]    })}... ...router.get('/detail', (req, res, next) => {    const id = req.query.id    const result = getDetail(id)    return result.then(data => {        res.json(            new SuccessModel(data)        )    })})

Redis 连接

/db/redis.js:

const redis = require('redis')const { REDIS_CONF } = require('../conf/db')// 创建客户端const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)redisClient.on('ready', res => {    console.log('redis启动成功', res)})redisClient.on('error', err => {    console.log('redis启动失败', err)})module.exports = {    redisClient}

6. 路由处理

/routes/里包含了blog和用户的路由处理。例如:

get请求:

router.get('/list', (req, res, next) => {    let author = req.query.author || ''    const keyword = req.query.keyword || ''     const result = getList(author, keyword)    return result.then(listData => {        res.json({            errno: 0,            listData        })    })})

post 请求:

router.post('/update', (req, res, next) => {    const id = req.query.id    const result = updateBlog(id, req.body)    return result.then(val => {        if (val) {            res.json({                errno: 0,                msg: "更新成功"            })        } else {            res.json({                errno: 0,                msg: "更新失败"            })        }    })})

res.send() 和 res.json() 和 res.end() 和 res.set()

express 路由中根据不同的响应头字段,有不同的响应方式:

· res.render()

主要用来渲染 views 中的前端模板文件,对于前后端分离的项目,暂时不需要

· res.send([body])

用来发送HTTP响应。该body参数可以是一个Buffer对象、字符串、数组或对象。

express 针对不同参数,发出的相应行为也不一样:

  • 当参数为 Buffer 对象时,res.send()方法将 Content-Type 响应头字段设置为“application/octet-stream”
  • 当参数为 String 时,res.send()方法将 Content-Type 响应头字段设置为“text/html”
  • 当参数为 Array 或 Object 对象时,res.send()方法将 Content-Type 响应头字段设置为“application/json”

如下:

res.send({name: "cedric"});header: Content-Type: application/json; charset=utf-8body:{"name":"cedric"}res.send(["name","cedric"]);header: Content-Type: application/json; charset=utf-8body:["name","cedric"]res.send('hello world');header: Content-Type: text/html; charset=utf-8body:hello worldres.send(new Buffer('abc'));header:Content-Type: application/octet-streambody:

res.json([body])

  • 发送一个json的响应, 相当于原生 Node 的: res.end(JSON.stringify(data))
  • 将Content-Type 响应头字段设置为: Content-Type: application/json; charset=utf-8
  • 该方法res.send()与将对象或数组作为参数相同
  • 不过,res.json() 可以将其他值转换为JSON,例如null、undefined、String

· res.end()

结束响应过程, 用于快速结束没有任何数据的响应

· res.set()

用来设置 header ‘content-type’参数。

// 即使res.send 参数是数组或对象,也可以通过res.set()将 Content-Type 响应头字段设置为“text/html”res.set('Content-Type', 'text/html');res.send({name: "cedric"});header: Content-Type: text/html; charset=utf-8body:'{"name":"cedric"}'// 即使res.send 参数是字符串,也可以通过res.set()将 Content-Type 响应头字段设置为“application/json”res.set('Content-Type', 'application/json');res.send('hello world');header: Content-Type: application/json; charset=utf-8body:hello world

Http 协议是一个无状态协议, 客户端每次发出请求, 请求之间是没有任何关系的。但是当多个浏览器同时访问同一服务时,服务器怎么区分来访者哪个是哪个呢?cookie、session、token 就是来解决这个问题的。详情参考:

本项目通过 cookie + session 机制处理登录,并通过 Redis 存储 session 数据。

依赖:

$ npm i express-session$ npm i redis connect-redis

在 app.js 中配置:

··· ···const session = require('express-session')const RedisStore = require('connect-redis')(session)··· ···// 处理 cookieapp.use(cookieParser());··· ···const redisClient = require('./db/redis').redisClientconst sessionStore = new RedisStore({  client: redisClient})app.use(session({    secret: 'CEdriC_#18603193', // 密匙可以随意添加,建议由大写+小写+加数字+特殊字符组成    cookie: {        path: '/', // 默认配置        httpOnly: true, // 默认配置,只允许服务端修改        maxAge: 24 * 60 * 60 * 1000 // cookie 失效时间 24小时    },    store: sessionStore  // 将 session 存入 redis}))

在 routes/user.js 中 登录路由时,设置 session:

router.post('/login', function (req, res, next) {    const { username, password } = req.body    const result = login(username, password)    return result.then(data => {        if (data.username) {            // 登录时 设置 session, 然后被connect-redis同步到redis            req.session.username = data.username            req.session.realname = data.realname            res.json(                new SuccessModel('登录成功')            )        }        res.json(            new ErrorModel('用户名和密码错误,登录失败')        )    })})

登录校验 中间件

/middleware/loginCheck.js:

const { ErrorModel } = require('../model/resModel')module.exports = (req, res, next) => {    if (req.session.username) {        // 登陆成功,需执行 next(),以继续执行下一步        next()        return    }    // 登陆失败,禁止继续执行,所以不需要执行 next()    res.json(        new ErrorModel('未登录')    )}

用新增、删除、更改blog时,都需要验证是否登录:

使用示例如下:

// 新建blog, 通过中间件进行登录验证router.post('/new', loginCheck, (req, res, next) => {    req.body.author = req.session.username     const result = newBlog(req.body)    return result.then(data => {        res.json(            new SuccessModel(data)        )    })})

8. 日志处理

一般项目中,在开发环境下,将日志直接打印在控制台记录;生成环境(线上)下,需要将日志写入指定的文件下,如访问日志、错误日志、事件追踪日志等。

express 中主要使用 中间件处理日志,app.js 文件已经默认引入了改中间件,使用app.use(logger('dev'))可以将请求信息打印在控制台,便于开发进行调试,但实际生产环境中,需要将日志记录在logs目录里,可以使用如下代码:

var path = require('path');var fs = require('fs')var logger = require('morgan'); // 中间件,生成日志// 处理日志const ENV = process.env.NODE_ENVif (ENV !== 'production') {  // 如果是开发环境 / 测试环境,则直接在控制台终端打印 log 即可  app.use(logger('dev'));} else {  // 如果当前是线上环境,则将请求日志写入/logs/access.log文件中,其他日志(错误日志和事件追踪日志也做类似处理)  const logFileName = path.join(__dirname, 'logs', 'access.log')  const writeStream = fs.createWriteStream(logFileName, {    flags: 'a'  })  app.use(logger('combined', {    stream: writeStream  }))}

日志分析

  • 如:针对日志 access.log,分析 chrome 的占比
  • 日志按行存储,一行就是一条日志
  • 通过 node.js readline 进行逐行分析

/utils/readline.js:

const fs = require('fs')const path = require('path')const readline = require('readline')// 文件名const fileName = path.join(__dirname, '../', '../', 'logs', 'access.log')// 创建 read streamconst readStream = fs.createReadStream(fileName)// 创建 readline 对象const rl = readline.createInterface({    input: readStream})let chromeNum = 0let sum = 0// 逐行读取rl.on('line', (lineData) => {    if (!lineData) {        return    }    // 记录总行数    sum++    const arr = lineData.split(' -- ')    if (arr[2] && arr[2].indexOf('Chrome') > 0) {        // 累加 chrome 的数量        chromeNum++    }})// 监听读取完成rl.on('close', () => {    console.log(chromeNum, sum)    console.log('chrome 占比:' + chromeNum / sum)})

9. Nginx 反向代理

参考

10. 安全防御

SQL 注入

SQL 注入,一般是通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。

SQL 注入预防措施

使用 mysql 的 escape 函数处理输入内容即可

在所有输入 sql 语句的地方,用 escape 函数处理一下即可, 例如:

const login = (username, password) => {    // 预防 sql 注入    username = escape(username)    password = escape(password)    const sql = `        select username, realname from users where username=${username} and password=${password};    `    return exec(sql).then(rows => {        return rows[0] || {}    })}

XSS 攻击

XSS 是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码(代码包括HTML代码和客户端脚本)植入到提供给其它用户使用的页面中。

XSS 攻击预防措施

转换升级 js 的特殊字符

$ npm install xss

然后修改:

const xss = require('xss')const title = data.title // 未进行 xss 防御const title = xss(data.title) // 已进行 xss 防御

然后如果在 input 输入框 恶意输入 <script> alert(1) </script>, 就会被转换为下面的语句并存入数据库:

&lt;script&gt; alert(1) &lt;/script&gt;,已达到无法执行 <script> 的目的。

注:

更多预防攻击措施可参考:

11. 密码加密

/utils/cryp.js

const crypto = require('crypto')// 密匙const SECRET_KEY = '这个密钥可以随意填写'// md5 加密function md5(content) {    let md5 = crypto.createHash('md5')    return md5.update(content).digest('hex')}// 加密函数function genPassword(password) {    const str = `password=${password}&key=${SECRET_KEY}`    return md5(str)} module.exports = {    genPassword}

使用:

const { genPassword } = require('../utils/cryp')const login = (username, password) => {    // 预防 sql 注入    username = escape(username)    // 生成加密密码    password = genPassword(password)     password = escape(password)     const sql = `        select username, realname from users where username=${username} and password=${password};    `    return exec(sql).then(rows => {        return rows[0] || {}    })}

12. 线上部署与配置:PM2

线上部署通过 PM2, 详情请参考:

转载于:https://www.cnblogs.com/cckui/p/11021606.html

你可能感兴趣的文章
香港最新失业率2.8% 劳工市场短期内料维持偏紧状态
查看>>
央行:中国金融风险总体收敛
查看>>
山西警方破获快递跨省运输贩毒案
查看>>
河北省政协十二届二次会议开幕
查看>>
沈阳国际冰雪季以“冰棋园”演绎冰雪“棋”迹
查看>>
为什么 Python 4.0 会与 Python 3.0 不同?
查看>>
Android无处不在,Android开发者大有可为
查看>>
Nodejs:使用Mongodb存储和提供后端CRD服务
查看>>
Dubbo配置直连
查看>>
一个小白的四次前端面试经历
查看>>
Hybrid App技术解析 -- 原理篇
查看>>
前端也要学系列:设计模式之策略模式
查看>>
【译】SQL 指引:如何写出更好的查询
查看>>
细说 Java 的深拷贝和浅拷
查看>>
go配置文件读取
查看>>
通过项目梳理vuex模块化 与vue组件管理
查看>>
每天阅读一个 npm 模块(2)- mem
查看>>
React Native 中的状态栏
查看>>
Python 抓取微信公众号账号信息
查看>>
Spring源码系列:依赖注入-引言
查看>>