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
响应,说明前后是一个用户。