简介
大家都知道,HTTP
是一个无状态的协议,那么Web应用要怎么保持用户的登录态呢?
如果你对cookie
,sessio
n和token
的优缺点不太明白,或者你想知道在实际中到底怎么实现登录态,那么本文将非常适合你,本文将以发展历程为顺序为大家介绍cookies
,session
以及token
的优势和缺点。
本文知识点:
cookie
,session
,token(json web token,jwt)
的区别node
中jwt
的应用
正文
我们站在服务器这一端,一个用户请求过来怎么判断他有没有登录呢?
在验证用户名和密码之后,我们可以发给客户端一个凭证(isLogin = true),如果请求中有这个凭证,那么他就是登陆之后的用户。 cookie
和session
的区别在于,凭证的存储位置。换言之,如果凭证存储在客户端,那就是cookie
。如果凭证存储在服务端,那就是session
。
客户端存储(cookie)
cookie
其实是HTTP
头部的一个字段,本质上可以存储任何信息,早年用于实现登录态,所以有了一层别的含义——客户端存储。把凭证存储到cookie
中,每次浏览器的请求会自动带上cookie
里的凭证,方便服务端校验,就像下面这样:
图1 海绵宝宝请求调用`/login`接口,派大星验证通过后给海绵宝宝颁发的登录凭证`isLogin=true`
但是这样面临的问题是:
用户本人可以通过修改document.cookie="isLogin = true"
伪造登陆凭证:
图2 海绵宝宝直接修改cookie跳过登录接口验证获取数据
服务端存储(session)
session
本意是指客户端与服务器的会话状态,由于凭证存储到了服务端,后来也把这些存在服务端的信息称为session
。
现在服务器决定自己维护登录状态,仅发给客户端一个key
,然后在自己维护一个key-value
表,如果请求中有key
,并且在表中可以找到对应的value
,则视为合法:
图3 海绵宝宝请求调用`/login`接口,派大星验证通过后给海绵宝宝颁发`sessionID`
这样即使海绵宝宝自行修改了sessionID
,在派大星那里没有对应的记录,也无法获取数据。
session
是一个好的解决方案,但是他的问题是:如果存在多个服务器如负载均衡时,每个服务器的状态表必须同步,或者抽离出来统一管理,如使用Redis
等服务。
Token
还有其他的方法可以实现登陆态吗?
cookie
方法不需要服务器存储,但是凭证容易被伪造,那有什么办法判断凭证是否伪造呢?
和HTTPS
一样,我们可以使用签名的方式帮助服务器校验凭证。
JSON Web Token(简称JWT)
是以JSON
格式存储信息的Token
,其结构图如下:
图4 JSON Web Token结构图
JWT
由3部分构成:头部,负载和签名。
根据官网介绍:
- 头部存储
Token
的类型和签名算法(上图中,类型是jwt
,加密算法是HS256
) - 负载是
Token
要存储的信息(上图中,存储了用户姓名和昵称信息) - 签名是由指定的算法,将转义后的头部和负载,加上密钥一同加密得到的。
最后将这三部分用.
号连接,就可以得到了一个Token
了。
使用JWT
维护登陆态,服务器不再需要维护状态表,他仅给客户端发送一个加密的数据token
,每次请求都带上这个加密的数据,再解密验证是否合法即可。由于是加密的数据,即使用户可以修改,命中几率也很小。
客户端如何存储token
呢?
- 存在
cookie
中,虽然设置HttpOnly
可以有效防止XSS
攻击中token
被窃取,但是也就意味着客户端无法获取token
来设置CORS
头部。 - 存在
sessionStorage
或者localStorage
中,可以设置头部解决跨域资源共享问题,同时也可以防止CSRF
,但是就需要考虑XSS
的问题防止凭证泄露。
Node
中JWT
的使用
在Node
中使用JWT
只需要两步:
第一步,在你的/login
路由中使用jsonwebtoken
中间件用于生成token
:
const jwt = require('jsonwebtoken')
let token = jwt.sign({
name: user name
}, config.secret, {
expiresIn: '24h'
})
res.cookie('token', token)
复制代码
具体使用方法请查看jsonwebtoken
的Github
第二步,在Node
的入口文件app.js
中注册express-jwt
中间件用于验证token
:
const expressJwt = require('express-jwt')
app.use(expressJwt({
secret: config.secret,
getToken: (req) => {
return req.cookies.token || null
}
}).unless({
path: [
'/login'
]
}))
复制代码
如果getToken
返回null
,中间件会抛出UnauthorizedError
异常:
app.use(function (err, req, res, next) {
//当token验证失败时会抛出如下错误
if (err.name === 'UnauthorizedError') {
res.status(401).json({
status: 'fail',
message: '身份校验过期,请重新登陆'
});
}
});
复制代码
具体使用语法参考express-jwt
的Github
如何实现单点登录
假设我们在电脑和手机都使用同一个用户登陆,对于服务器来说,这两次登陆生成的token
都是合法的,尽管他们是同一个用户。所以两个token
不会失效。
要实现单点登陆,服务器只需要维护一张userId
和token
之间映射关系的表。每次登陆成功都刷新token
的值。
在处理业务逻辑之前,使用解密拿到的userId
去映射表中找到token
,和请求中的token
对比就能校验是否合法了。
图5 实现单点登录
总结
实现登录态是前端非常基础且重要的技能之一。之前在学习这一块的时候,分不清Cookie
,Session
和Token
的区别。session
是比cookie
更好的一种解决方案。token
成为主流,是因为他不需要额外的存储管理。但是当涉及到单点登录的时候,其实也出现了多个服务器需要同步映射表的问题。
欢迎大家在评论区讨论,指正!
参考
- 朴灵。《深入浅出nodejs》。P181
- shanyue。jwt 实践以及与 session 对比