Node.js:session & JWT
session
HTTP协议是无状态的,客户端的每次HTTP请求都是独立的,多个请求之间没有直接的关系,服务器不会保留每次HTTP请求的状态。
简单来说,当服务器接收到多个请求时,无法确定这些请求是否来自于同一用户,无法完成身份标识。为此,浏览器提供了cookie的方法,来标识一个客户的信息。
cookie
cookie是存储在用户浏览器中的一段不超过4 kb的字符串,其由键值对,以及其他几个控制cookie有效期、安全性、使用范围的可选属性构成。
访问baidu.com,进入控制面板,查看Application的Cookies,可以看到baidu.com对应的信息:

右侧这些内容,都是baidu.com使用的cookie。其中name和value是键值对存储信息,其余的是一些控制信息。
浏览器会记录用户在每个域名下访问时的cookie信息,并保存下来。当客户端发起请求时,浏览器会自动把该域名下的所有未过期的cookie一同发送到服务器。
也就是说,每个域名都有自己独立的cookie,并且浏览器会自动完成发送。

浏览器发送请求时,如果服务器响应了cookie信息,那么后续浏览器发送其他请求,只要属于同一域名,都会自动把之前收到的cookie一起发送给服务器。这样服务器就可以通过请求得知,哪些请求属于同一个用户。
但是cookie是一种不安全的身份验证机制,一方面用户可以伪造一个cookie发送给服务器,另一方面cookie在报文中是完全可见的,如果存储账号密码这样的信息,就很容易泄露。
session
为了解决用户伪造信息的问题,于是有了session认证机制,session比cookie多一个检验步骤,当用户发送一个带有cookie的请求时,服务端不是简单的接收请求,而是会判断这个cookie是否合法。

如图,当用户发起登录请求时,服务端生成一个cookie字符串,发送回给客户端。后续客户端每次通信,都要把cookie携带在请求内,当客户端收到请求后,验证客户端的cookie是否合法,只有信息合法才会发送响应。
在这个过程中,客户端全程都只持有一个加密后的字符串,而服务端持有用户的相关信息。只要服务端拿到字符串后,就会去查找这个字符串对应的信息,然后做出后续操作。因为客户端并不接触到具体信息,所以这个认证方式比cookie安全很多。
express-session
在Node.js中,express也提供了非常方便的中间件express-session,可以直接操作session。
下载:
npm i express-session
配置中间件:
const express = require('express')
const app = express()
// 请配置 Session 中间件
const session = require('express-session')
app.use(
session({
secret: 'hello world',
resave: false,
saveUninitialized: true,
})
)
通过require导入这个模块后,就可以直接使用了。express-session是一个中间件,需要绑定到app上,绑定时需要传入三个参数,其中后两个参数是固定写法,第一个参数secret是加密字符串,可以随意填写一个字符串。session会把信息基于该字符生成session id。
当成功配置中间件后,req会多出一个属性session:
app.get('/login', (req, res) => {
if (req.query.name !== 'root' || req.query.password !== '123456') {
return res.send({ status: 1, msg: '登录失败' })
}
req.session.name = req.query.name // 用户的信息
req.session.password = req.query.password // 用户的密码
req.session.islogin = true // 用户的登录状态
res.send({ status: 0, msg: '登录成功' })
})
当服务器接收到来自客户端的请求,先进行简单的账号密码验证,此处固定写为root 123456,具体业务中可能还涉及到数据库的查询操作。
如果登录成功,就把相关信息写入到req.session中,随后session会基于之前的secret字符串生成一个session id,最后send发送数据时,把session id作为cookie发送给客户端。
在浏览器访问/login:

可以看到,127.0.0.1下多出了一个cookie,其内容为connect.sid,这就是session id。这个connect.id内部,是一个看似乱码的字符串,其不包含session真正存储的信息,真正的信息存储在服务器上。
写一个/test路由,用于测试session:
app.get('/test', (req, res) => {
if (!req.session.islogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send("你好 " + req.session.name)
})
在这个路由内部,会检测该用户是否带有session.islogin,也就是该用户是否登录了。想要这个if成立,那么客户端发起请求时就必须带上session id,服务器会查找session id匹配的信息,并且存放在res.session对象中。
浏览器访问:

这次访问,没有在url尾部添加相关信息,但是服务器依然知道当前是root用户在操作,就是因为检测到了session id。
当用户退出时,服务器可以主动销毁session:
app.get('/logout', (req, res) => {
req.session.destroy()
res.send("退出成功")
})
当执行req.session.destroy时,服务器会销毁这个域对应的session信息,注意不是销毁所有的session,只销毁发起请求的req对应的信息。
当用户访问/logout后,可能浏览器内还是存有session id,但是这个session id已经失效了,如果还需要继续访问,就要重写登录,得到一个新的session id。
JWT
session的认证机制,是基于cookie实现的,但是cookie不支持跨域访问,所以如果要跨域访问,就需要做很多额外的跨域操作。
而JWT可以解决跨域认证问题,全称JSON Web Token,在需要面对跨域时,推荐使用JWT完成身份认证。
JWT的原理和session很类似:

当用户提交指定的信息,服务器就会生成一个token字符串,并把这个字符串发送回给客户端,客户端把token存储到LocalStorage或SessionStorage中。后续客户端发起请求,只要携带这个token,服务端就可以通过token获取到用户信息。
JWT和session的有以下区别:
- 加密后的字符串的存储位置不同
session的信息存储在服务器本地,JWT的信息存储在加密的字符串中session由浏览器自主携带,但是JWT需要前端发送ajax请求时手动添加
因为session使用cookie存储加密后的字符串,cookie不支持跨域。所以JWT就不再使用cookie存储加密后的字符串了,而是使用浏览器的本地临时存储。
token字符串由三部分组成,分别是Header头部、Payload有效载荷、Signature签名,三部分之间由.分隔。
其中Header和Signature是安全性相关的部分,而Payload才是真正存储的数据
express-jwt & jsonwebtoken
在Node.js中使用JWT,需要使用两个相关的包:
jsonwebtoken:用于生成token字符串express-jwt:用于将token字符串还原为json对象
安装包:
npm i jsonwebtoken express-jwt
- 定义密钥
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
const secretKey = '@#$%fhalkfjwpw@#$%'
在对数据加密时,要使用一个字符串的密钥,这个密钥可以提前定义到一个变量中,并且越复杂越好。
- 生成
token
生成token字符串,可以通过jwt.sign方法进行加密
语法:
jwt.sign({ key: value }, secretKey, { expiresIn: 'times' })
其第一个参数传入要加密的对象,第二个参数传入使用的密钥,第三个参数是一个配置对象,对象中的expiresIn属性定义了数据的过期时间,超过该时间后,数据无效。
最后该方法会返回加密完成后的字符串。
示例:
app.post('/login', function (req, res) {
if (req.body.name !== 'root' || req.body.password !== '123456') {
return res.send({
status: 400,
message: '登录失败!',
})
}
const tokenStr = jwt.sign({ username: req.body.name }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
以上示例中,将信息req.query.username加密为了字符串tokenStr,有效期60s。最后通过res.send发送回给客户端,token属性内部传入加密后的字符串。
使用postman发送请求:

这样就可以得到服务端生成的token
- 读取
token
读取token使用express-jwt中间件,先把中间件注册到app中:
app.use(expressJWT({ secret: secretKey }))
注册中间件时,第一个参数传入一个对象,对象内部的secret属性指定之前指定的密钥。
expressJWT中间件会检测请求的token是否合法,并且解析。以上代码,将所有路由都注册了这个中间件,但是用户在/login之前是没有得到token,这个中间件不能对/login注册:
app.use(expressJWT({ secret: secretKey }).unless({ path: "/login" }))
当注册中间件成功后,req下就会多出一个user属性,这个属性内部是token字符串解析出来的内容。
app.get('/test', function (req, res) {
res.send("你好" + req.user.username)
})
其中req.user就是expressJWT解析出来的对象,这个对象内部有之前写入的username属性。
使用postman发送post /test请求:

在请求头中,要携带一个Authorization选项,选项的内容是Bearer token字符串。注意在Bearer与token之间,有一个空格。这个添加请求头的过程,浏览器并不完成,而是由客户端的ajax完成。
最后得到你好 root响应,说明前后是一个用户。










