文章目录
- ✨文章有误请指正,如果觉得对你有用,请点三连一波,蟹蟹支持😘
- 前言
- 登录鉴权Cookie&Session
- ExpressSession中间件
- MVC演示
- 登录鉴权JSON Web Token (JWT)
- Jsonwebtoken参数
- sign 方法
- verify 方法
- 封装JsonWebToken
- 校验异常情况Verify
- 阿贾克斯Interceptors
- 代码实现步骤 演示
- 1、访问Login 获取打开登录页面
- 2、输入登录密码完成登录(登录成功设置token)
- 3、登录成功后端设置(请求拦截器成功前 设置localStorage token) 并且返回响应头(header)
- 4、首页发送数据接口请求...
- 5、App 入口
- 总结
✨文章有误请指正,如果觉得对你有用,请点三连一波,蟹蟹支持😘
⡖⠒⠒⠒⠤⢄⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸ ⠀⠀⠀⡼⠀⠀⠀⠀ ⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⣲⡴⣗⣲⡦⢤⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠋⠉⠉⠓⠛⠿⢷⣶⣦⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠘⡇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡴⠊⠉⠳⡄⠀⢀⣀⣀⡀⠀⣸⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠰⠆⣿⡞⠉⠀⠀⠉⠲⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⢧⡀⣀⡴⠛⡇⠀⠈⠃⠀⠀⡗⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣱⠃⡴⠙⠢⠤⣀⠤⡾⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⣇⡼⠁⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣸⢠⣉⣀⡴⠙⠀⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠈⠁⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣀⠤⠚⣶⡀⢠⠄⡰⠃⣠⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣠⠔⣋⣷⣠⡞⠀⠉⠙⠛⠋⢩⡀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀
⠀⡏⢴⠋⠁⠀⣸⠁⠀⠀⠀⠀⠀ ⠀⣹⢦⣶⡛⠳⣄⠀⠀⠀⠀⠀
⠀⠙⣌⠳⣄⠀⡇ 不能 ⡏⠀⠀ ⠈⠳⡌⣦⠀⠀⠀⠀
⠀⠀⠈⢳⣈⣻⡇ 白嫖 ⢰⣇⣀⡠⠴⢊⡡⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠳⢿⡇⠀⠀⠀⠀⠀⠀⢸⣻⣶⡶⠊⠁⠀⠀
⠀⠀⠀⠀⠀⢠⠟⠙⠓⠒⠒⠒⠒⢾⡛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⠏⠀⣸⠏⠉⠉⠳⣄⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⡰⠃⠀⡴⠃⠀⠀⠀⠀⠈⢦⡀⠈⠳⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣸⠳⣤⠎⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⡤⢯⡀⠀⠀⠀⠀⠀⠀
⠀⠐⡇⠸⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⢳⠀⠀⠀⠀⠀⠀
⠀⠀⠹⡄⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⠸⡆⠀⠀⠀⠀⠀
⠀⠀⠀⠹⡄⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⣧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢹⡤⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣷⠚⣆⠀⠀⠀⠀
⠀⠀⠀⡠⠊⠉⠉⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡎⠉⠀⠙⢦⡀⠀
⠀⠀⠾⠤⠤⠶⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠒⠲⠤⠽
前言
Node.js
是一个javascript运行环境。它让javascript可以开发后端程序
,实现几乎其他后端语言实现的所有功能,可以与```PHP、Java、Python、.NET、Ruby等后端语言平起平坐。- Nodejs是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的JS解释,但是Node之父
Ryan Dahl
把这V8搬到了服务器上,用于做服务器的软件。
登录鉴权Cookie&Session
鉴权说明 :「HTTP 无状态」我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的 「标记」
,那解决办法是什么呢?
Cookie&Session 图示 ↓
- 使用库 : npm install express-session 、 npm install connect-mongo
- expressSession NPM :https://www.npmjs.com/package/express-session
- connectmongo NPM :https://www.npmjs.com/package/connect-mongo
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 })
})
}
演示
-
缺点
- 内容过大时会裂开
- CSRF伪造
登录鉴权JSON Web Token (JWT)
-
session 缺点 :占内存,占空间,容易跨平台伪造 ,CSRF伪造攻击
-
JSON Web Token (JWT) 使用步骤说明 :session 换成 localstorage ,数据储存在localstorage,防止加密的数据(
签名
)被反推出来 需要 添加 密钥。
使用Session 图示
使用JSON Web Token (JWT) 图示
- 总结
-
当然, 如果一个人的token 被别人偷走了, 那也没办法,会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。
-
这样一来, 就不保存session id , 只是生成token , 然后验证token , 用CPU计算时间获取session 存储空间 !
-
解除了session id这个负担, 可以说是无事一身轻, 机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!
缺点
-
占带宽,正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多;
-
无法在服务端注销,那么久很难解决劫持问题;
-
性能问题,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包括以下选项
- algorithm:加密算法(默认值:HS256)
- expiresIn:支持秒表示或描述时间跨度zeit / ms的字符串。如60,“2 days”,“10h”,“7d“ 过期时间
- notBefore:定义在什么时间之前,该jwt都是不可用的。支持秒表示或描述时间跨度zeit / ms的字符串。如:60,“2days”,“10h”,“7d”
- issuer:jwt签发者
- jwtid: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- subject:jwt所面向的用户
- noTimestamp
- header
- keyid
- mutatePayload
4、callback(err,token):生成jwt结束时执行的回调函数,遵循nodejs的错误优先原则,第一个参数是生成jwt过程抛出的异常信息,第二个参数是成功生成的jwt字符串值
❗注意 : 需要注意的是,一旦指定了sign方法的第四个参数回调函数,则sign方法变为异步方法,jwt字符串只能通过回调函数获取。
verify 方法
–
说明:verify方法用于校验和解析jwt字符串,该方法入参四个参数,如👇
1、token:jwt字符串
2、secret:加密密钥
3、options:配置对象 配置如下
- algorithms 签名加密算法,默认为HS256
- audience 受众
- complete 默认为false,表示只返回payload解密数据,若为true表示返回{ payload, 1. header, signature }解密数据
- issuer 签发人
- ignoreExpiration if true do not validate the expiration of the token.
- ignoreNotBefore if true do not validate the not before of the token.
- subject 主题
- clockTolerance number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
- maxAge the maximum allowed age for tokens to still be valid.
- clockTimestamp the time in seconds that should be used as the current time for all necessary comparisons.
- 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
- NotBeforeError 该异常发生在jwt尚未生效时使用
- TokenExpiredError 该异常发生在jwt字符串校验成功,但是已失效
- 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的相关知识点,一点一滴的记录了下来,有问题请评论区指正,共同进步,这才是我写文章的原因之,如果这篇文章对您有帮助请三连支持一波