node-博客开发

一、用inspect协议实现chrom断点测试(以koa2创建的项目为例)

  1. 在项目的package.json文件中配置:--inspect=端口

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uol6d9Dt-1614615183723)(/Users/mr.jiang/Desktop/我的笔记/Typora相关图片/前端/inspect-1.png)]

  2. 运行:npm run dev

  3. 在chrom浏览器输入:chrome://inspect/#devices,打开调试页面

二、server端和前端的区别

  1. 服务稳定性
    • server端可能会遭受各种恶意进攻和误操作
    • PM2:进程守护,一旦服务挂掉,就会自动重启服务
  2. 考虑内部和cpu(优化、扩展)
    • server端要承载很多请求,cpu和内存都是稀缺资源
    • 使用stream写日志,使用redis存session
  3. 日志记录
    • 前端是日志发起方,不关心后续
    • server要记录日志、存储、分析
  4. 安全
    • server端要随时准备接受各种恶意攻击
    • eg:越权操作,数据库攻击等
    • 登陆验证,预防xss攻击和sql注入
  5. 集群和服务拆分
    • 通过扩展及服务拆分承载大流量

三、开发博客项目之接口

​ 处理get、post请求参数,存放到:req.query、req.body中

  1. http概述

    1. 客户端:DNS解析,建立TCP(三次握手)连接,发送http请求

      • DNS解析:根据域名去找相应ip地址

        查找过程:1.本地host是否有这个网址映射关系 -> 2.本地DNS解析器缓存 -> 3.本地DNS服务器 -> 4.迭代查询:根域服务器 -> 顶级域cn -> 第二层域hb.cn -> 子域www.hb.cn

      • TCP三次握手:本地如何连接远程相应ip地址。

    2. server接收到http请求,处理,并返回数据

    3. 客户端接收到返回数据,处理数据(渲染页面,执行js)

  2. 处理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成功");
    
  3. 处理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成功");
    
    
  4. 处理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成功");
    
  5. 搭建开发环境

    安装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"
      },
      
  6. 分层设计:项目目录结构分析

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EL8u6k2-1614615183724)(…/…/Typora相关图片/前端/node-mulu.png)]

    • www.js:启动服务,配置相关服务的端口号。

    • app.js:加载注册各种业务组件。

      1. 将get、post方式的请求参数分别存放到req.query、req.body中,方便Router中的js文件使用

      2. 将req、res传递给相关Router,然后将Router中返回的数据返回到页面相关接口中。

        //处理user 路由
        const userData = handleUserRouter(req, res);
        if(userData) {
            res.end(JSON.stringify(userData));
            eturn
        }
        
      3. 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中。

  1. cookie、session、redis初步了解

    • cookie是实现登陆的必要基础,session是登陆的解决方案
    • redis是内存数据库,mysql是硬盘数据库
    • session写入redis中
  2. 客户端查看、编辑cookie。

    • 客户端查看cookie的三种方式:

      1. network中查看http请求:
        • Request Header中携带cookie
        • 若服务端修改cookie,Request Headers就会有Set-Cookies相关信息
      2. Application->Storage->Coolies查看
      3. document.cookie
    • 客户端js编辑cookie(有限制):document.cookie = 'key=value'

  3. 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: "请先做登陆"
           }));
        }
    }
    
  4. 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("登陆失败");
                  }
              });
      
  5. 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("登陆失败");
            }
          });
      }
      
  6. 从session到redis

    1. web server(服务端)中存放session的弊端:

      • 内存过大(seseion存放很多用户信息),会把进程的空间挤爆(操作系统给每个进程设置内存块,有最大内存限制)。
      • 多线程之间(启动多个node程序运行当前项目),服务端会初始化多个session,导致数据无法共享。
  7. redis存放sesion的优点:

    • 访问频繁,对性能要求高(在运行内存中存放性能高,mysql是在硬盘内存中性能差些)
      • 数据量不会太大
      • 可以不考虑断电丢失数据问题(mysql要考虑)
      • 数据可以共享,多个进程都是从一个redis中取session缓存。
  8. 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();
      })
      
  9. 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数据空间中。

六、博客项目之日志

  1. 日志存放在文件中原因:

    • 对性能要求不高
    • 体积较大
    • 不同设备查看成本低

    日志类型:

    • 访问日志access log(server端最重要的日志)
    • 自定义日志(包含自定义事件、错误日志)
  2. 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("文件存在")
       }
    })
    
  3. 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) => {});
  4. stream操作文件

    1. 利用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("拷贝完成");
    })
    
    1. 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);
    
  5. 利用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()}`);
    
  6. crontab(linux系统中的定时器)实现日志拆分

    1. 编写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
      
    2. 命令行,进入crontab编写定时执行sh的脚本

      #1、进入crontab
      crontab -e
      
      #2、编写crontab定时器脚本:
      #语法:分钟 小时 日期 月份 星期 sh '要执行sh脚本所在路径'
      * 0 * * * sh "sh脚本所在路径" 
      #表示每条0点执行sh脚本
      
      #3、:wq保存crontab脚本,并退出
      #查看当前有哪些crontab任务:crontab -l
      
  7. 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内容
  • 密码加密:保障用户信息安全(重要!)
  1. 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函数就不需要单引号包裹
    
  2. 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);
      
  3. 密码加密:

    • 万一数据库被黑客攻破,拿到用户密码,会去破解用户其他软件的账号密码。

    ​ 解决:

    • 注册的时候对用户密码进行加密

      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
      }
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值