✨文章有误请指正,如果觉得对你有用,请点三连一波,蟹蟹支持😘

                    ⡖⠒⠒⠒⠤⢄⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸   ⠀⠀⠀⡼⠀⠀⠀⠀ ⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⣲⡴⣗⣲⡦⢤⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠋⠉⠉⠓⠛⠿⢷⣶⣦⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠘⡇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡴⠊⠉⠳⡄⠀⢀⣀⣀⡀⠀⣸⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠰⠆⣿⡞⠉⠀⠀⠉⠲⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⢧⡀⣀⡴⠛⡇⠀⠈⠃⠀⠀⡗⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣱⠃⡴⠙⠢⠤⣀⠤⡾⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⣇⡼⠁⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣸⢠⣉⣀⡴⠙⠀⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠈⠁⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣀⠤⠚⣶⡀⢠⠄⡰⠃⣠⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣠⠔⣋⣷⣠⡞⠀⠉⠙⠛⠋⢩⡀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀
⠀⡏⢴⠋⠁⠀⣸⠁⠀⠀⠀⠀⠀ ⠀⣹⢦⣶⡛⠳⣄⠀⠀⠀⠀⠀
⠀⠙⣌⠳⣄⠀⡇   不能   ⡏⠀⠀  ⠈⠳⡌⣦⠀⠀⠀⠀
⠀⠀⠈⢳⣈⣻⡇   白嫖 ⢰⣇⣀⡠⠴⢊⡡⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠳⢿⡇⠀⠀⠀⠀⠀⠀⢸⣻⣶⡶⠊⠁⠀⠀
⠀⠀⠀⠀⠀⢠⠟⠙⠓⠒⠒⠒⠒⢾⡛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⠏⠀⣸⠏⠉⠉⠳⣄⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⡰⠃⠀⡴⠃⠀⠀⠀⠀⠈⢦⡀⠈⠳⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣸⠳⣤⠎⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⡤⢯⡀⠀⠀⠀⠀⠀⠀
⠀⠐⡇⠸⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⢳⠀⠀⠀⠀⠀⠀
⠀⠀⠹⡄⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⠸⡆⠀⠀⠀⠀⠀
⠀⠀⠀⠹⡄⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⣧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢹⡤⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣷⠚⣆⠀⠀⠀⠀
⠀⠀⠀⡠⠊⠉⠉⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡎⠉⠀⠙⢦⡀⠀
⠀⠀⠾⠤⠤⠶⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠒⠲⠤⠽   

前言

  1. Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与```PHP、Java、Python、.NET、Ruby等后端语言平起平坐。
  2. Nodejs是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的JS解释,但是Node之父 Ryan Dahl在这里插入图片描述把这V8搬到了服务器上,用于做服务器的软件。

登录鉴权Cookie&Session

鉴权说明 :「HTTP 无状态」我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的 「标记」 ,那解决办法是什么呢?

Cookie&Session 图示 ↓

在这里插入图片描述

   ExpressSession中间件

//注册session中间件
app.use(session({
  name: "先生", //session名字
  secret: "serverz~qwer", //服务器生成 session 的签名
  cookie: {
    maxAge: 1000 * 60 * 60, //过期时间
    secure: false // 为 true 时候表示只有 https 协议才能访问cookie
  },
  rolling: true, 为 true 表示 超时前刷新,cookie 会重新计时; 为 false 表示在超时前刷新多少次,都是按照第一次刷新开始计时。
  resave: true, //重新设置session后, 会自动重新计算过期时间
  saveUninitialized: true, 强制将为初始化的 session 存储
  
  //通过connect-mongo 存储数据库、过期自动销毁、创建新的sesstion储存数据库
  store: MongoStore.create({ 
    mongoUrl: 'mongodb://127.0.0.1:27017/Oyande', //新创建了一个数据库
    ttl: 1000 * 60 * 60 // 过期时间
  })
}))

   MVC演示

– 登录校验接口

//登录校验接口
router.post("/login", UserController.login)
router.get("/logout", UserController.logout)

APP.js管理
//设置中间件,sesssion过期校验
app.use((req, res, next) => {
  //排除login相关的路由和接口
  if (req.url.includes("login")) {
    next()
    return
  }

  if (req.session.user) {
    //重新设置以下sesssion
    req.session.mydate = Date.now()
    next()
  } else {
    //是接口, 返回错误码
    //不是接口,就重定向
    req.url.includes("api")
      ? res.status(401).json({ ok: 0 }) : res.redirect("/login")
  }
})

– M

    login: (username, password) => {
        return UserModel.find({ username, password })
    }

– V

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>登录页面</h1>

    <div>
        <div>用户名:<input id="username" /></div>
        <div>密码:<input type="password" id="password" /></div>
        <div><button id="login">登录</button></div>
    </div>

    <script>
        var username = document.querySelector("#username")
        var password = document.querySelector("#password")
        var login = document.querySelector("#login")

        login.onclick = () => {
            console.log(username.value, password.value)
            fetch("/api/login", {
                method: "POST",
                body: JSON.stringify({
                    username: username.value,
                    password: password.value,
                }),
                headers: {
                    "Content-Type": "application/json"
                }
            }).then(res => res.json()).then(res => {
                console.log(res)
                if (res.ok === 1) {
                    location.href = "/"
                } else {
                    alert("用户名密码不匹配")
                }
            })
        }
    </script>
</body>

</html>

– C

  login: async (req, res) => {
    const { username, password } = req.body
    const data = await UserService.login(username, password)
    if (data.length === 0) {
      res.send({
        ok: 0
      })
    } else {
      //设置session {}  
      req.session.user = data[0] //设置session对象, 
      //默认存在内存中。
      res.send({
        ok: 1
      })
    }
  },

  logout: (req, res) => {
    req.session.destroy(() => {
      res.send({ ok: 1 })
    })
  }

演示

在这里插入图片描述

  • 缺点

    1. 内容过大时会裂开
    2. CSRF伪造

登录鉴权JSON Web Token (JWT)

  • session 缺点 :占内存,占空间,容易跨平台伪造 ,CSRF伪造攻击

  • JSON Web Token (JWT) 使用步骤说明 :session 换成 localstorage ,数据储存在localstorage,防止加密的数据(签名)被反推出来 需要 添加 密钥。

使用Session 图示

在这里插入图片描述

使用JSON Web Token (JWT) 图示

在这里插入图片描述

  • 总结
  1. 当然, 如果一个人的token 被别人偷走了, 那也没办法,会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。

  2. 这样一来, 就不保存session id , 只是生成token , 然后验证token , 用CPU计算时间获取session 存储空间 !

  3. 解除了session id这个负担, 可以说是无事一身轻, 机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!

缺点

  1. 占带宽,正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多;

  2. 无法在服务端注销,那么久很难解决劫持问题;

  3. 性能问题,JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。

注意

  • CSRF攻击的原因是浏览器会自动带上cookie,而不会带上token;

  • 以CSRF攻击为例 ↓

    cookie:用户点击了链接,cookie未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作;token:用户点击链接,由于浏览器不会自动带上token,所以即使发了请求,后端的token验证不会通过,所以不会进行扣款操作;

  • 使用库 Jsonwentoken : https://www.npmjs.com/package/jsonwebtoken

  • 使用库 axios : https://www.npmjs.com/package/axios

   Jsonwebtoken参数

sign 方法

说明:sign方法用于生成一个jwt字符串,该方法接收四个参数,依次如 👇

0、jsonwebtoken.sign(payload, secretOrPrivateKey, [options, callback]);

参数

1、接收一个对象,用于传入用户身份信息

2、接收一个字符串,作为jwt.signature的加密密钥

3、options包括以下选项

  1. algorithm:加密算法(默认值:HS256)
  2. expiresIn:支持秒表示或描述时间跨度zeit / ms的字符串。如60,“2 days”,“10h”,“7d“ 过期时间
  3. notBefore:定义在什么时间之前,该jwt都是不可用的。支持秒表示或描述时间跨度zeit / ms的字符串。如:60,“2days”,“10h”,“7d”
  4. issuer:jwt签发者
  5. jwtid: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
  6. subject:jwt所面向的用户
  7. noTimestamp
  8. header
  9. keyid
  10. mutatePayload

4、callback(err,token):生成jwt结束时执行的回调函数,遵循nodejs的错误优先原则,第一个参数是生成jwt过程抛出的异常信息,第二个参数是成功生成的jwt字符串值

❗注意 : 需要注意的是,一旦指定了sign方法的第四个参数回调函数,则sign方法变为异步方法,jwt字符串只能通过回调函数获取。

verify 方法

说明:verify方法用于校验和解析jwt字符串,该方法入参四个参数,如👇

1、token:jwt字符串

2、secret:加密密钥

3、options:配置对象 配置如下

  1. algorithms 签名加密算法,默认为HS256
  2. audience 受众
  3. complete 默认为false,表示只返回payload解密数据,若为true表示返回{ payload, 1. header, signature }解密数据
  4. issuer 签发人
  5. ignoreExpiration if true do not validate the expiration of the token.
  6. ignoreNotBefore if true do not validate the not before of the token.
  7. subject 主题
  8. clockTolerance number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
  9. maxAge the maximum allowed age for tokens to still be valid.
  10. clockTimestamp the time in seconds that should be used as the current time for all necessary comparisons.
  11. nonce if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens.

4、callback(err,decode):回调函数,遵循nodejs错误优先原则,第一个参数是异常信息,第二个参数是根据jwt解码出来的数据

❗注意1 : 需要注意其中complete属性,该属性为true,则根据jwt字符串解码出来完整数据,即包含header,payload,signature,否则只包含payload

❗注意2 : 一旦verify传入callback,verify方法不再同步返回解码数据,但是verify入参callback并不是异步执行的,而是同步执行的。

   封装JsonWebToken

规范文件路径 util/JWT.js

const jwt = require("jsonwebtoken")
const secret = "guoxiansheng"
const JWT = {
	//获取的token 格式 XXX.XXX.XXX
    generate(value, expires) {
        return jwt.sign(value, secret, { expiresIn: expires }) //内容数据、密钥、token的时长
        //第四个回调函数(err,token)=>{} 
    },
    verify(token) {
        try {
            return jwt.verify(token, secret,{complete:false}) //加密的token 、 密钥
			//第四个回调函数(err,token)=>{} 
        } catch (error) {
            return false
        }
    }
}
module.exports = JWT

   校验异常情况Verify

  1. NotBeforeError 该异常发生在jwt尚未生效时使用
  1. TokenExpiredError 该异常发生在jwt字符串校验成功,但是已失效
  1. JsonWebTokenError 该异常发生在
规范文件路径 util/JWT.js

const jwt = require("jsonwebtoken")
const secret = "guoxiansheng"
const JWT = {
	//获取的token 格式 XXX.XXX.XXX
    generate(value, expires) {
        return jwt.sign(value, secret, { expiresIn: expires,notBefore:'0.2h' }) //内容数据、密钥、token的时长
        //第四个回调函数(err,token)=>{} 
    },
    verify(token) {
        try {
            return jwt.verify(token, secret,{complete:false}) //加密的token 、 密钥
			//第四个回调函数(err,token)=>{} 
        } catch (error) {
            return false
        }
    }
}
module.exports = JWT

   阿贾克斯Interceptors

   <script>
        //拦截器,
        //请求发出前,执行的方法
        axios.interceptors.request.use(function (config) {
            return config;
        }, function (error) {
            return Promise.reject(error);
        });

		//请求成功后 ,第一个调用的方法
        axios.interceptors.response.use(function (response) {
            return response;
        }, function (error) {
            return Promise.reject(error);
        });
    </script>

   代码实现步骤 演示

1、访问Login 获取打开登录页面

html

<!DOCTYPE html>
<html lang="en">

<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
   <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
   <script>
       //拦截器,
       axios.interceptors.request.use(function (config) {
           return config;
       }, function (error) {
           return Promise.reject(error);
       });

       axios.interceptors.response.use(function (response) {
           const { authorization } = response.headers
           authorization && localStorage.setItem("token", authorization)
           return response;
       }, function (error) {
           return Promise.reject(error);
       });
   </script>
</head>

<body>
   <h1>登录页面</h1>

   <div>
       <div>用户名:<input id="username" /></div>
       <div>密码:<input type="password" id="password" /></div>
       <div><button id="login">登录</button></div>
   </div>

   <script>
       var username = document.querySelector("#username")
       var password = document.querySelector("#password")
       var login = document.querySelector("#login")

       login.onclick = () => {
           axios.post("/api/login", {
               username: username.value,
               password: password.value,
           }).then(res => {
               console.log(res.data)
               if (res.data.ok === 1) {
                   location.href = "/"
               } else {
                   alert("用户名密码不匹配")
               }
           })
       }
   </script>
</body>

</html>

接口

//使用到ejs
var express = require('express');
var router = express.Router();

router.get('/', function (req, res, next) {
  res.render('login', { title: 'Express' });
});

module.exports = router;

2、输入登录密码完成登录(登录成功设置token)

登录判断

 login: async (req, res) => {
   const { username, password } = req.body
   const data = await UserService.login(username, password)
   if (data.length === 0) {
     res.send({
       ok: 0
     })
   } else {
     //设置token 
     const token = JWT.generate({
       _id: data[0]._id,
       username: data[0].username
     }, "1d")
     //token返回在header
     res.header("Authorization", token)
     //默认存在内存中。
     res.send({
       ok: 1
     })
   }
 },

3、登录成功后端设置(请求拦截器成功前 设置localStorage token) 并且返回响应头(header)

   <script>
       //拦截器,
       axios.interceptors.request.use(function (config) {
           return config;
       }, function (error) {
           return Promise.reject(error);
       });

       axios.interceptors.response.use(function (response) {
           const { authorization } = response.headers
           authorization ?.localStorage.setItem("token", authorization)
           return response;
       }, function (error) {
           return Promise.reject(error);
       });
   </script>

4、首页发送数据接口请求…

说明:首页发送数据接口请求,且 首页设置 “阿贾克斯” 请求拦截器,请求前(获取localStorage token) 且 设置请求头(headers),app.js(设置中间件判断token,通过后返回响应头) , 请求成功前(重新设置localStorage token),出错(移除token,重定向到 /login)

前端代码

<!DOCTYPE html>
<html>

<head>
 <title>
   首页
 </title>
 <link rel='stylesheet' href='/stylesheets/style.css' />
 <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 <script>
   //请求发出前,执行的方法
   axios.interceptors.request.use(function (config) {
     const token = localStorage.getItem("token")
     config.headers.Authorization = `Bearer ${token}`
     return config;
   }, function (error) {
     return Promise.reject(error);
   });

   // 请求成功后 ,第一个调用的方法
   axios.interceptors.response.use(function (response) {
     const {
       authorization
     } = response.headers
     authorization && localStorage.setItem("token", authorization)
     return response;
   }, function (error) {
     if (error.response.status === 401) {
       localStorage.removeItem("token")
       location.href = "/login"
     }
     return Promise.reject(error);
   });
 </script>
</head>
<body>
 <script>
   //获取列表
   axios.get("/api/user?page=1&limit=10").then(res => {
     res = res.data
     var tbody = document.querySelector("tbody")
     tbody.innerHTML = res.map(item => `
         <tr>
           <td>${item._id}</td>  
           <td>${item.username}</td>  
           <td>${item.age}</td>  
         </tr>
       `).join("")
   })
 </script>
</body>

</html>

5、App 入口

//设置中间件,token过期校验
app.use((req, res, next) => {
  //排除login相关的路由和接口
  if (req.url.includes("login")) {
    next()
    return
  }

  const token = req.headers["authorization"]?.split(" ")[1]
  if (token) {
    const payload = JWT.verify(token)
    if (payload) {
      //重新计算token过期时间
      const newToken = JWT.generate({
        _id: payload._id,
        username: payload.username
      }, "1d")
      res.header("Authorization", newToken)
      next()
    } else {
      res.status(401).send({ errCode: -1, errInfo: "token过期" })
    }
  } else {
    next()
  }
})

总结

以上是个人学习Node的相关知识点,一点一滴的记录了下来,有问题请评论区指正,共同进步,这才是我写文章的原因之,如果这篇文章对您有帮助请三连支持一波

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐