https://github.com/caochangkui/node-express-koa2-project/tree/master/blog-expresshtml
Nginx 反向代理前端
使用 express-generator 初始化項目node
跨平臺環境變量設置:mysql
$ npm install cross-env --save-dev
安裝文件監測工具 nodemon:nginx
$ 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" // 開發環境下,文件監測 }
啓動項目:git
$ npm run dev
├── 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 加密處理
項目從開發、測試、預發佈到生成環境(線上)的環境變量通常都是不一樣的,爲避免每次都手動修改,這裏先配置環境變量github
/conf/db.js:web
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, }
/db/mysql.js:redis
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 查詢:sql
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) ) }) })
/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 }
/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: "更新失敗" }) } }) })
express 路由中根據不一樣的響應頭字段,有不一樣的響應方式:
主要用來渲染 views 中的前端模板文件,對於先後端分離的項目,暫時不須要
用來發送HTTP響應。該body參數能夠是一個Buffer對象、字符串、數組或對象。
express 針對不一樣參數,發出的相應行爲也不同:
以下:
res.send({name: "cedric"}); header: Content-Type: application/json; charset=utf-8 body:{"name":"cedric"} res.send(["name","cedric"]); header: Content-Type: application/json; charset=utf-8 body:["name","cedric"] res.send('hello world'); header: Content-Type: text/html; charset=utf-8 body:hello world res.send(new Buffer('abc')); header:Content-Type: application/octet-stream body:<Buffer 61 62 63>
結束響應過程, 用於快速結束沒有任何數據的響應
用來設置 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-8 body:'{"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-8 body:hello world
Http 協議是一個無狀態協議, 客戶端每次發出請求, 請求之間是沒有任何關係的。可是當多個瀏覽器同時訪問同一服務時,服務器怎麼區分來訪者哪一個是哪一個呢?cookie、session、token 就是來解決這個問題的。詳情參考:http://www.javashuo.com/article/p-cggfdplt-kq.html
本項目經過 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) ··· ··· // 處理 cookie app.use(cookieParser()); ··· ··· const redisClient = require('./db/redis').redisClient const 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) ) }) })
通常項目中,在開發環境下,將日誌直接打印在控制檯記錄;生成環境(線上)下,須要將日誌寫入指定的文件下,如訪問日誌、錯誤日誌、事件追蹤日誌等。
express 中主要使用 morgan 中間件處理日誌,app.js 文件已經默認引入了改中間件,使用app.use(logger('dev'))
能夠將請求信息打印在控制檯,便於開發進行調試,但實際生產環境中,須要將日誌記錄在logs目錄裏,可使用以下代碼:
var path = require('path'); var fs = require('fs') var logger = require('morgan'); // 中間件,生成日誌 // 處理日誌 const ENV = process.env.NODE_ENV if (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 })) }
/utils/readline.js:
const fs = require('fs') const path = require('path') const readline = require('readline') // 文件名 const fileName = path.join(__dirname, '../', '../', 'logs', 'access.log') // 建立 read stream const readStream = fs.createReadStream(fileName) // 建立 readline 對象 const rl = readline.createInterface({ input: readStream }) let chromeNum = 0 let 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) })
參考 http://www.javashuo.com/article/p-cpdetwfi-kn.html
SQL 注入,通常是經過把 SQL 命令插入到 Web 表單提交或輸入域名或頁面請求的查詢字符串,最終達到欺騙服務器執行惡意的 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 是一種在web應用中的計算機安全漏洞,它容許惡意web用戶將代碼(代碼包括HTML代碼和客戶端腳本)植入到提供給其它用戶使用的頁面中。
轉換升級 js 的特殊字符
$ npm install xss
而後修改:
const xss = require('xss') const title = data.title // 未進行 xss 防護 const title = xss(data.title) // 已進行 xss 防護
而後若是在 input 輸入框 惡意輸入 <script> alert(1) </script>
, 就會被轉換爲下面的語句並存入數據庫:
<script> alert(1) </script>
,已達到沒法執行 <script>
的目的。
注:
更多預防攻擊措施可參考:http://www.javashuo.com/article/p-ddhcytfc-kh.html
/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] || {} }) }
線上部署經過 PM2, 詳情請參考:http://www.javashuo.com/article/p-uumvhdwc-eo.html