登录态问题

简介

大家都知道,HTTP是一个无状态的协议,那么Web应用要怎么保持用户的登录态呢?

如果你对cookiesessiontoken的优缺点不太明白,或者你想知道在实际中到底怎么实现登录态,那么本文将非常适合你,本文将以发展历程为顺序为大家介绍cookiessession以及token的优势和缺点。

本文知识点:

  1. cookiesessiontoken(json web token,jwt)的区别
  2. nodejwt的应用

正文

我们站在服务器这一端,一个用户请求过来怎么判断他有没有登录呢?

在验证用户名和密码之后,我们可以发给客户端一个凭证(isLogin = true),如果请求中有这个凭证,那么他就是登陆之后的用户。 cookiesession的区别在于,凭证的存储位置。换言之,如果凭证存储在客户端,那就是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部分构成:头部,负载和签名。

根据官网介绍

  1. 头部存储Token的类型和签名算法(上图中,类型是jwt,加密算法是HS256
  2. 负载是Token要存储的信息(上图中,存储了用户姓名和昵称信息)
  3. 签名是由指定的算法,将转义后的头部和负载,加上密钥一同加密得到的。

最后将这三部分用.号连接,就可以得到了一个Token了。

使用JWT维护登陆态,服务器不再需要维护状态表,他仅给客户端发送一个加密的数据token,每次请求都带上这个加密的数据,再解密验证是否合法即可。由于是加密的数据,即使用户可以修改,命中几率也很小。

登录态问题

客户端如何存储token呢?

  1. 存在cookie中,虽然设置HttpOnly可以有效防止XSS攻击中token被窃取,但是也就意味着客户端无法获取token来设置CORS头部。
  2. 存在sessionStorage或者localStorage中,可以设置头部解决跨域资源共享问题,同时也可以防止CSRF,但是就需要考虑XSS的问题防止凭证泄露。

NodeJWT的使用

Node中使用JWT只需要两步:

第一步,在你的/login路由中使用jsonwebtoken中间件用于生成token

复制代码
const jwt = require('jsonwebtoken')
let token = jwt.sign({
      name: user name
    }, config.secret, {
      expiresIn: '24h'
    })
    res.cookie('token', token)

具体使用方法请查看jsonwebtokenGithub

第二步,在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-jwtGithub

如何实现单点登录

假设我们在电脑和手机都使用同一个用户登陆,对于服务器来说,这两次登陆生成的token都是合法的,尽管他们是同一个用户。所以两个token不会失效。

要实现单点登陆,服务器只需要维护一张userIdtoken之间映射关系的表。每次登陆成功都刷新token的值。

在处理业务逻辑之前,使用解密拿到的userId去映射表中找到token,和请求中的token对比就能校验是否合法了。

登录态问题

图5 实现单点登录

总结

实现登录态是前端非常基础且重要的技能之一。之前在学习这一块的时候,分不清CookieSessionToken的区别。session是比cookie更好的一种解决方案。token成为主流,是因为他不需要额外的存储管理。但是当涉及到单点登录的时候,其实也出现了多个服务器需要同步映射表的问题。

欢迎大家在评论区讨论,指正!

补充:=》blog.csdn.net/qq_40147756…

1,cookie 的出现
浏览器和服务器之间的传输使用的 HTTP 协议,而它是无状态的。也就是说,每个请求都是独立的,服务器并不知道2次请求是否是同一个人。

为了解决这个问题,服务器想了一个办法:

当客户端登录成功后,服务器会给客户端一个令牌凭证 token;客户端后续的请求都需要带着这个 token 在服务器做验证。

但用户不可能只在一个网站登录,于是客户端会收到各个网站的出入证 token。所以客户端需要一个 “卡包” 来实现以下功能:

能够存放多个 token,token 可能来自不同的网站,也可能一个网站有多个 token。
能够自动出示正确的 token,客户端访问不同网站时,会自动在请求中带着对应的 token。
管理 token 的有效期,客户端需要自动发现那些过期的 token 并移除。
满足上述要求的就是 cookie,每一个 token 就是一个 cookie。

每个网站的 cookie 大小不超过 4kb。

2,cookie 的组成
每一个 cookie 都记录了以下信息:(除了 key 和 value,其他非必填+顺序无关)

key:键,比如表示身份编号的字符串 token

value:值,比如 123abc,它可以是任何字符串。

domain:主机(域),表示这个 cookie 是属于哪个网站的,比如 www.csdn.net。【默认值:当前主机,也就是 location.host】MDN参考

path:路径,表示这个 cookie 是属于该网站的哪个路径。【默认值:实测发现是 cookie 所处目录的上级目录。比如页面是 http://localhost:3001/a/api/login,则 path 为 /a/api】

secure:是否使用安全传输。MDN参考

httpOnly:表示该 cookie 仅能用于传输,而客户端通过 document.cookie 获取的是空字符串,这对防止跨站脚本攻击(XSS)会很有用。

XSS:比如当前页面打开 iframe,iframe 可以获取父级的 cookie。设置 httponly 可以不允许 js 获取来防止跨站脚本攻击。

expires:过期时间,表示该 cookie 在什么时候过期。MDN 参考

max-age:有效期。【默认值:如果 expires 和 max-age 都不设置,则为 session,也就是会话结束后过期,大多浏览器关闭(注意不是标签页关闭)意味着会话结束。如果设置其中一个,cookie 会保存在硬盘中,即便电脑关闭也不会消失。】

expires 和 max-age 一般只设置一个即可。

浏览器自动发送 cookie 的条件
需要同时满足以下4个条件:

没有过期。
expires 必须是一个有效的GWT时间,格林威治标准时间字符串,比如 Fri, 22 Dec 2023 17:09:13 GMT。到期后浏览器会自动删除。
new Date().toGMTString() // Fri, 22 Dec 2023 17:09:13 GMT
// 对比常见的时间格式:
new Date() // Sat Dec 23 2023 01:09:13 GMT+0800 (中国标准时间)

max-age 是相对有效期,比如 max-age=1000,相当于设置 expires=当前时间 + 1000s
domain 字段和这次请求的域是匹配的。
设置的 domain 是 csdn.net,则可匹配的请求域有:csdn.net、www.csdn.net、blogs.csdn.net等。
设置的 domain 是www.csdn.net,则只能匹配 www.csdn.net 这样的请求域。
cookie 是不关心端口的,只要域匹配即可。(所以端口不同导致非同源而产生的跨域并不影响。)
无效的域,浏览器的是不认的。比如对 search.jd.com/Search?keyw… 网页来说:

【翻译:通过 Set-Cookie 标头设置 cookie 的尝试被阻止,因为其域对于当前域无效】

path 字段和这次请求的 path 也是匹配的。/ 表示匹配所有。如果是 /docs:
匹配的路径:/docs,/docs/,/docs/Web/,/docs/Web/HTTP
不匹配的路径:/,/docsets,/fr/docs
secure 字段验证。设置该字段,则请求协议必须是 https(否则不发送 cookie);不设置则请求协议可以是 https 或 http。
浏览器会将符合条件的 cookie,自动添加到请求头 Cookie 中。下图可以看到有3个满足的 cookie,以 ; 分隔。

3,设置 cookie
cookie 是保存在浏览器端的,有2种设置模式:

服务器设置:通过设置响应头 set-cookie: 123abc,浏览器会自动保存在 “卡包” 中。查看方式:控制台–>Application–> Storage–>Cookies
浏览器设置:这种情况比较少见。举例:用户关闭了广告时勾选了【不喜欢】或其他原因,就可以把这种小信息直接通过 js 保存到 cookie 中。后续请求服务器时,服务器会根据这个信息调整广告投放。
3.1,服务端设置
可在一次响应中设置多个 cookie。格式如下:

键=值; path=?; domain=?; expires=?; max-age=?; secure; httponly
1
举例:

// 服务端
const Koa = require(“koa”);
const Router = require(“koa-router”);
const { bodyParser } = require(“@koa/bodyparser”);

const app = new Koa();
const router = new Router();

router.post(“/api/login”, (ctx) => {
const { name, pwd } = ctx.request.body;
if (name === “下雪天的夏风” && pwd === “123”) {
ctx.set(“set-cookie”, ‘token=aaa; domain=localhost; max-age=3600;secure; httponly’);
ctx.body = “登录成功”;
} else {
ctx.body = {
code: 500,
msg: “用户名或密码错误”,
};
}
});

router.get(“/api/home”, (ctx) => {
ctx.body = “home”;
});

app.use(bodyParser()).use(router.routes());
app.listen(3000);



提交

form 表单发送请求登录成功后,会自动跳转到页面 http://localhost:3000/api/login,可以看到 cookie 已经设置了:

注意到 path 的默认值是 cookie 所处目录的上级目录。
expires/max-age 的时间格式保存为 ISO国际标准时间
new Date() // Sat Dec 23 2023 01:27:53 GMT+0800 (中国标准时间)
new Date().toISOString() // 2023-12-22T17:27:53.738Z
new Date().toGMTString() // Fri, 22 Dec 2023 17:27:53 GMT

再次访问 http://localhost:3000/api/home 时,会发现请求头中自动带上了 cookie:

3.1,客户端设置
格式和在服务端相同,只是 httponly 字段无效。因为该字段本来就是限制在客户端访问的,客户端设置它没有意义。

document.cookie = ‘token=aaa; domain=localhost;secure;httponly’

3.3,删除 cookie
可以修改 cookie 的过期时间即可:max-age=-1。浏览器会自动删除。

可以让服务器响应一个同样的 domain、同样的 path、同样的 key,只是时间过期的 cookie 即可。

以上面的例子来说,设置如下:

ctx.set(“set-cookie”, ‘token=aaa; domain=localhost; max-age=-1’);
1
或客户端删除:

document.cookie = ‘token=aaa; domain=localhost; max-age=-1’
1
注意:无论是修改还是删除,都需要注意 domain 和 path,因为可能存在 domain 和 path 不同但 key 相同的 cookie。

4,使用流程总结
登录 / 注册请求:

浏览器发送用户名和密码到服务器。
服务器验证通过后,在响应头中设置 cookie,附带登录认证信息(一般为 jwt)。
浏览器收到 cookie 保存下来。
后续请求,浏览器会自动将符合的 cookie 附带到请求中;服务器验证 cookie 后,允许其他操作完成业务流程。
————————————————

参考

  1. 朴灵。《深入浅出nodejs》。P181

  2. shanyue。jwt 实践以及与 session 对比

原文链接:https://juejin.cn/post/7356788983483842587 作者:阳光多一些

(0)
上一篇 2024年4月13日 上午11:03
下一篇 2024年4月13日 上午11:13

相关推荐

发表回复

登录后才能评论