一、用inspect协议实现chrom断点测试(以koa2创建的项目为例)
-
在项目的package.json文件中配置:
--inspect=端口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uol6d9Dt-1614615183723)(/Users/mr.jiang/Desktop/我的笔记/Typora相关图片/前端/inspect-1.png)]
-
运行:
npm run dev
-
在chrom浏览器输入:
chrome://inspect/#devices
,打开调试页面
二、server端和前端的区别
- 服务稳定性
- server端可能会遭受各种恶意进攻和误操作
- PM2:进程守护,一旦服务挂掉,就会自动重启服务
- 考虑内部和cpu(优化、扩展)
- server端要承载很多请求,cpu和内存都是稀缺资源
- 使用stream写日志,使用redis存session
- 日志记录
- 前端是日志发起方,不关心后续
- server要记录日志、存储、分析
- 安全
- server端要随时准备接受各种恶意攻击
- eg:越权操作,数据库攻击等
- 登陆验证,预防xss攻击和sql注入
- 集群和服务拆分
- 通过扩展及服务拆分承载大流量
三、开发博客项目之接口
处理get、post请求参数,存放到:req.query、req.body中
-
http概述
-
客户端:DNS解析,建立TCP(三次握手)连接,发送http请求
-
DNS解析:根据域名去找相应ip地址
查找过程:
1.本地host是否有这个网址映射关系
->2.本地DNS解析器缓存
->3.本地DNS服务器
->4.迭代查询:根域服务器 -> 顶级域cn -> 第二层域hb.cn -> 子域www.hb.cn
-
TCP三次握手:本地如何连接远程相应ip地址。
-
-
server接收到http请求,处理,并返回数据
-
客户端接收到返回数据,处理数据(渲染页面,执行js)
-
-
处理get请求
-
客户端向服务端索取数据时,用get请求
-
querystring.parse( ):将字符串拼接请求参数 转成 相应对象
const http = require('http'); const querystring = require("querystring"); const server = http.createServer((req, res) => { let url = req.url; let obj = querystring.parse(url.split("?")[1]); res.end(JSON.stringify(obj)); }) server.listen(8423) console.log("get成功");
-
-
处理post请求
- req.method:获取请求类型 POST GET
- req.headers[“content-type”]
- Data stream:通过数据流的形式接收数据,如果数据量很大,服务端要像水流一样一点一点的接收
- chunk:接收到的一点点数据,每次来数据都会触发data事件,数据接收完毕后触发end事件
- res.end( ):必须返回一个字符串,所以要用JSON.stringify()将其转为json字符串格式的数据。
const http = require('http'); const server = http.createServer((req, res) => { if(req.method == 'POST') { console.log("content-type", req.headers["content-type"]); let postData = ''; req.on('data', (chunk) => { postData += chunk.toString();//chunk必须转成字符串格式 }) req.on('end', () => { console.log('postData', postData); res.end("hellow world"); //res.end必须返回一个字符串 }) } }) server.listen(8877); console.log("post成功");
-
处理post get请求综合例子
- 设置接口返回格式为JSON:
res.setHeader("Content-type", "application/json");
,作用:当返回格式是字符串类型j的son数据,浏览器会自动将数据转成json格式的数据。
const http = require("http"); const server = http.createServer((req, res) => { const method = req.method; const url = req.url; const path = req.url.split("?")[0]; const query = req.url.split("?")[1]; res.setHeader("Content-type", "application/json"); const resDatas = { method, url, path, query } if(method == "GET") { res.end(JSON.stringify(resDatas)); } if(method == "POST") { let postData = ""; req.on("data", (chunk) => { postData += chunk; }) req.on("end", () => { resDatas.postData = postData; res.end(JSON.stringify(resDatas)); }) } }); server.listen('8999'); console.log("post-get成功");
- 设置接口返回格式为JSON:
-
搭建开发环境
安装nodemon、cross-env:
cnpm i nodemon cross-env --save-dev
-
使用nodemon监听文件变化,自动重启node
-
使用cross-env设置环境变量,兼容mac linux windows
"scripts": { "dev": "cross-env NODE_ENV=dev nodemon --inspect=9229 ./bin/www.js", "prod": "cross-env NODE_ENV=prod nodemon ./bin/www.js" },
-
-
分层设计:项目目录结构分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EL8u6k2-1614615183724)(…/…/Typora相关图片/前端/node-mulu.png)]
-
www.js:启动服务,配置相关服务的端口号。
-
app.js:加载注册各种业务组件。
-
将get、post方式的请求参数分别存放到req.query、req.body中,方便Router中的js文件使用
-
将req、res传递给相关Router,然后将Router中返回的数据返回到
页面
或相关接口
中。//处理user 路由 const userData = handleUserRouter(req, res); if(userData) { res.end(JSON.stringify(userData)); eturn }
-
promise方式处理POST请求参数,并将参数存放到req.body中
const getPostDatas = (req) => { let promise = new Promise((resolve, reject) => { if(req.method !== "POST") { resolve({}); return } if(req.headers["content-type"] !== "application/json") { resolve({}); return; } let postData = ""; req.on("data", (chunk) => { postData += chunk.toString(); }); req.on("end", () => { if(!postData) { resolve({}); return } resolve(JSON.parse(postData)); }); }) return promise; } getPostDatas(req).then((postData) => { //将post请求参数放到req.body中 req.body = postData; //获取query req.query = querystring.parse(url.split("?")[1]); })
-
-
Router: 只处理负责路由相关的,对Controller中获取到的数据进行包装,返回给app.js使用。
const {SuccessModel, ErrorModel} = require("../model/resModel"); const {checkLogin} = require("../controller/user"); const handleUserRouter = (req, res) => { const method = req.method; const url = req.url; const path = req.path; if (method === "POST" && path === "/api/user/login") { const {name, password} = req.body; if (checkLogin(name, password)) { return new SuccessModel({msg: "登陆成功"}); } else { return new ErrorModel("登陆失败"); } } } module.exports = handleUserRouter;
-
Controller:只负责处理数据(如何将请求参数用上,去数据库进行数据的处理,拿到处理后的数据return 出去)。不关心数据要如何进行包装,不关心数据是返回给哪个路由。
const checkLogin = (name, password) => { if(name == "jiangyf" && password == "123456") { return true; }else { return false; } } module.exports = { checkLogin }
-
model(数据模型):包装数据,对要返回给前端的数据进行包装,达到数据格式统一的效果。
class BaseModel { constructor(data, message) { if(typeof data == "string") { this.message = data; data = null; message = null; } if(data) { this.data = data; } if(message) { this.message = message; } } } class SuccessModel extends BaseModel { constructor(data, message) { super(data, message); this.errno = 0; } } class ErrorModel extends BaseModel { constructor(data, message) { super(data, message); this.errno = -1; } }
-
四、开发博客项目之数据存储
五、博客项目之登陆
将tokenId存放到req.sessionId,将用户信息存放到key值是tokenId的req.session中。
-
cookie、session、redis初步了解
- cookie是实现登陆的必要基础,session是登陆的解决方案
- redis是内存数据库,mysql是硬盘数据库
- session写入redis中
-
客户端查看、编辑cookie。
-
客户端查看cookie的三种方式:
- network中查看http请求:
- Request Header中携带cookie
- 若服务端修改cookie,Request Headers就会有Set-Cookies相关信息
- Application->Storage->Coolies查看
- document.cookie
- network中查看http请求:
-
客户端js编辑cookie(有限制):
document.cookie = 'key=value'
-
-
cookie用于登陆验证
if (method === "GET" && path === "/api/user/login-test") { if(req.cookie.username) { return Promise.resolve(new SuccessModel({ msg: "cookie有效果" })); }else { return Promise.resolve(new SuccessModel({ msg: "请先做登陆" })); } }
-
server端解析、编辑cookie
-
解析cookie
//解析cookie req.cookie = {}; const cookieStr = req.headers.cookie || ""; cookieStr.split(";").forEach((item) => { if(!item) { return } const arr = item.split("="); const key = arr[0].trim(); const value = arr[1]; req.cookie[key] = value; });
-
编辑cookie:
- 限制cookie只有后端才能修改:httpOnly:针对浏览器,限制js脚本(document.cookie = “username=‘编辑测试’”)操作cookie,只有服务端修改cookie才能生效。
- 过期时间:expires=过期时间。
- 生效路径:path=/,可以使cookie在所有路径都生效,不然会默认:api/user/login。
const getCookieExpires = () => { const d=new Date(); d.setTime(d.getTime() + (24 * 60 * 60 * 1000)); return d.toGMTString(); } if (method === "GET" && path === "/api/user/login") { const {name, password} = req.query; return checkLogin(name, password).then((resData) => { if(resData) { //登陆成功后设置cookie,当username是中文的时候可以用encodeURIComponent编译下,否则会报错 res.setHeader('Set-Cookie', `username=${encodeURIComponent(name)}; path=/; httpOnly; expires=${getCookieExpires()}`); return new SuccessModel({msg: "登陆成功"}); } else { return new ErrorModel("登陆失败"); } });
-
-
session相关:
-
用cookie做登陆验证:暴露username,很危险,可以利用session将登陆成功的用户信息存放到服务端。
-
利用session实现处理登陆验证原理:
- 服务端生成一个tokenid,当访问到项目路由的时候,将tokenid设置到客户端的cookie中。
- 定义一个SESSION_DATA对象,用之前生成的
tokenid作为对象的key值
,最后赋值给req.session
。 - 当用户访问登陆接口,并且登陆成功后,将用户相关信息存放到之前生成的key值(即:req.session)中。
//session 数据 let SESSION_DATA = {}; const serverHandle = (req, res) => { //第一步:解析session let needSetCookie = false; let tokenId = req.cookie.tokenid; if(tokenId) { if(!SESSION_DATA[tokenId]) { //cookie中tokenId存在的时候,用tokenId作为key值初始化存放用户信息的空间对象。 SESSION_DATA[tokenId] = {}; } }else { needSetCookie = true; tokenId = `${Date.now()}_${Math.random()}`; SESSION_DATA[tokenId] = {}; } req.session = SESSION_DATA[tokenId]; //第二步:在访问blog、user返回数据的地方都加上是否要设置cookie的操作 if(needSetCookie) { //当tokenId不存在的时候,要往客户端的cookie中设置tokenid。 res.setHeader('Set-Cookie', `tokenid=${encodeURIComponent(tokenId)}; path=/; httpOnly; expires=${getCookieExpires()}`); } } //第三步:当成功登陆后,将用户信息存放到指定session中。 if (method === "GET" && path === "/api/user/login") { const {name, password} = req.query; return checkLogin(name, password).then((resData) => { if(resData) { //登陆成功后往指定key值的session中存入:name、password req.session.name = name; req.session.password = password; return new SuccessModel({msg: "登陆成功"}); } else { return new ErrorModel("登陆失败"); } }); }
-
-
从session到redis
-
web server(服务端)中存放session的弊端:
- 内存过大(seseion存放很多用户信息),会把进程的空间挤爆(操作系统给每个进程设置内存块,有最大内存限制)。
- 多线程之间(启动多个node程序运行当前项目),服务端会初始化多个session,导致数据无法共享。
-
-
redis存放sesion的优点:
- 访问频繁,对性能要求高(在运行内存中存放性能高,mysql是在硬盘内存中性能差些)
- 数据量不会太大
- 可以不考虑断电丢失数据问题(mysql要考虑)
- 数据可以共享,多个进程都是从一个redis中取session缓存。
- 访问频繁,对性能要求高(在运行内存中存放性能高,mysql是在硬盘内存中性能差些)
-
node如何连接redis
-
const redis = require("redis"); //创建redis客户端 const redisClient = redis.createClient(6379, "localhost"); redisClient.on("error", (err) => { console.log(err); }) //测试 redisClient.set("name", 'linyq', redis.print); redisClient.get("name", (err, res) => { if (err) { console.log(err); } console.log(res); //退出 redisClient.quit(); })
-
-
App.js文件夹中使用redis解析session。
(1)用
req.sessionId
来存放tokenId
,用req.session
来存放登陆的用户信息。(2)如果url请求没有带tokenId那么生成一个,将其存放到req.sessionId中,并且用tokenId作为key值初始化redis存储空间
。(3)用req.sessionId去redis中查询相应的value值,拿到值后将其赋值给req.session,如果返回null则拿tokenId重新初始化redis存储空间
。//使用redis,解析session let tokenId = req.cookie.tokenid; let needSetCookie = false; if(!tokenId) { needSetCookie = true; tokenId = `${Date.now()}_${Math.random()}`; req.sessionId = tokenId; set(tokenId, {}); } req.sessionId = tokenId; get(req.sessionId).then((resData) => { if(resData == null) { set(req.sessionId, {}); req.session = {} } else { req.session = resData; } //将post读取数据的异步操作返回出去,方便等req.session赋值结束后,然后才去做post、get请求参数的获取操作。 return getPostDatas(req); })
在登陆成功的接口中
将用户信息存放到指定key(tokenId)的redis数据空间
中。
六、博客项目之日志
-
日志存放在文件中原因:
- 对性能要求不高
- 体积较大
- 不同设备查看成本低
日志类型:
- 访问日志access log(server端最重要的日志)
- 自定义日志(包含自定义事件、错误日志)
-
readFile( )、writeFile( ) 如何操作文件、判断文件是否存在
const fs = require("fs"); const path = require("path"); const filePath = path.resolve(__dirname, 'data.txt'); //1.读取文件 fs.readFile(filePath, (err, data) => { if(err) { console.log(err); return; } //data是二进制类型,要转成字符串类型 console.log(data.toString()); }) //2.写入文件 const content = "我是新写入内容"; const opt = { flag: "a", //往文件追加:“a”, 覆盖文件内容:“w”。 }; fs.writeFile(filePath, content, opt,(err) => { if(err) { console.log(err); } }) //3.判断文件是否存在 fs.exists(filePath, (exist) => { if(exist) { console.log("文件存在") } })
-
stream特点:
-
req、res类似stream流,可以直接用来建立管道连接:
req.pipe(res)
。 -
IO包含:
网络IO
和文件IO
-
相比cpu计算和内存读写,IO特点就是慢
-
如何在有限的硬件资源下提升IO的操作效率:用stream(流)。
-
readFile是一下子把文件读出来,createReadStream是一点一点读文件内容。
-
对比createWriteFile跟writeFile写入内容:
- fs.createWriteStream(filePath, {flag: 1}).write(“要写入的内容”);
- fs.writeFile(filePath, “要写入的内容”, {flag: 1}, (err) => {});
-
-
stream操作文件
- 利用pipe拷贝文件
const filePath1 = path.resolve(__dirname, "data1.txt"); const filePath2 = path.resolve(__dirname, "data2.txt"); //读取文件的stream对象 let readStream = fs.createReadStream(filePath1); //写入文件的stream对象 let writeStream = fs.createWriteStream(filePath2, { flag: 'a' }); //通过pipe,打通IO管道 readStream.pipe(writeStream); //监听拷贝过程 readStream.on("data", (chunk) => { console.log(chunk.toString()); }); //数据拷贝结束回调 readStream.on("end", () => { console.log("拷贝完成"); })
- get请求读取文件(stream)
let server = http.createServer((req, res) => { if(req.method == 'GET') { const fileName1 = path.resolve(__dirname, "data1.txt"); const readStream = fs.createReadStream(fileName1); readStream.pipe(res); //将res作为stream的dest } }) server.listen(8000);
-
利用Stream写日志
//1.第一步:创建until/log.js文件存放写日志方法 //生成writeStream function createWriteStream(fileName) { const filePath = path.join(__dirname, "../", "../", "/logs/", fileName); const writeStream = fs.createWriteStream(filePath, {flag: "a"}) return writeStream; } //写日志方法 function writeLog(writeStream, log) { writeStream.write(log+ '\n'); } //写访问日志 const accessWriteStream = createWriteStream("access.log"); function access(log) { writeLog(accessWriteStream, log) } //2.在app.js文件中接口处调用access方法写日志 //记录access log日志 access(`${req.method} -- ${req.url} ${req.headers["user-agent"]} -- ${Date.now()}`);
-
crontab(linux系统中的定时器)实现日志拆分
-
编写sh脚本:拷贝日志文件到日期格式的日志文件中,然后再清空原先日志文件
#!/bin/sh cd /Users/mr.jiang/Desktop/node_study/blog-01/logs cp access.log $(date +%Y-%m-%d).access.log echo "" > access.log
-
命令行,进入crontab编写定时执行sh的脚本
#1、进入crontab crontab -e #2、编写crontab定时器脚本: #语法:分钟 小时 日期 月份 星期 sh '要执行sh脚本所在路径' * 0 * * * sh "sh脚本所在路径" #表示每条0点执行sh脚本 #3、:wq保存crontab脚本,并退出 #查看当前有哪些crontab任务:crontab -l
-
-
readline,一行一行读取日志进行分析。
//例:利用readline分析acess文件中使用谷歌浏览器占比 //1.创建readStream let filePath = path.join(__dirname, "../", "../", "logs", "access.log"); const readStream = fs.createReadStream(filePath); //2.创建readline对象 const rl = readline.createInterface({ input: readStream }); //3.逐行读取filePath中的内容 let sum = 0, chromeNum = 0; rl.on("line", (lineData) => { if(!lineData) { return } sum ++; let res = lineData.split("--")[2]; if(res && res.indexOf("Chrome") != -1) { chromeNum ++; } }); rl.on("close", () => { console.log("使用chrome占比是:"+ chromeNum + sum); });
七、博客项目之安全
- sql注入:窃取数据库内容
- xxs攻击:窃取前端的cookie内容
- 密码加密:保障用户信息安全(重要!)
-
sql注入问题,用escape函数处理。
//1.当前sql语句存在问题,当用户名为: jiangyf' -- 的时候sql语句后面的密码验证会被'--'给注释。 let sql = `select * from blog_user where name='${name} ' and password='${password}'`; //2.如何处理:给name,password包上mysql.escape函数 let sql = `select * from blog_user where name=${escape(name)} and password=${escape(password)}`; //注:用了escape函数就不需要单引号包裹
-
xxs攻击:
-
攻击方式:在页面展示内容中掺杂js代码,以获取页面信息
-
预防措施:转换生成js的特殊字符
-
攻击场景:新增博客的时候写入:
<script>alert(document.cookie)</script>
,当其他人访问博客的时候就会将他人的cookie获取到,这样就可以通过cookie去查看他人的博客信息。
解决:
-
安装xxs:cnpm i xxs --save
-
在新增博客时,用xxs对标题、内容进行转义处理。
const xss = require("xss"); const title = xxs(blogData.title); const content = xxs(blogData.content);
-
-
密码加密:
- 万一数据库被黑客攻破,拿到用户密码,会去破解用户其他软件的账号密码。
解决:
-
注册的时候对用户密码进行加密
const crypto = require("crypto"); //密钥 const SECRET_KEY = "jiangyf_0725Key"; //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 }