登录态问题

简介

大家都知道,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

php 复制代码
复制代码
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

php 复制代码
复制代码
const expressJwt = require('express-jwt')
app.use(expressJwt({
  secret: config.secret,
  getToken: (req) => {
    return req.cookies.token || null
  }
}).unless({
  path: [
    '/login'
  ]
}))

如果getToken返回null,中间件会抛出UnauthorizedError异常:

php 复制代码
复制代码
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.netwww.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 对比

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax