node + koa实现session ID认证、token认证

session ID认证流程

  1. 用户向服务器发送用户名和密码。
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等。
  3. 服务器向用户返回一个session_id,写入用户的Cookie。
  4. 用户随后的每一次请求,都会通过Cookie将session_id传回服务器。
  5. 服务器收到session_id,找到前期保存的数据,由此得到用户的身份。

session ID认证的缺陷

如果是服务器集群,或者是跨域的服务导向架构,就要求session数据共享,每台服务器都能够读取session。

  • 一种解决方案是session数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,值久层万一挂了,就会单点失败。
  • 另一种方案是服务器索性不保存session数据了,所有数据都保存在客户端,每次请求都发回服务端。JWT就是这种方案的一个代表。

Node + koa实现Session ID认证

  1. 安装koa-session
js 复制代码
pnpm install koa-session
  1. 配置koa-session中间件
  1. 使用koa-session中间件, 登录成功后向session添加内容

设置session后,在这里我通过await ctx.session.manuallyCommit();手动提交。在官方文档中,将到配置文件中设置autoCommit:false时才需要使用手动提交,但没有使用await ctx.session.manuallyCommit();时,http接口响应头中没有set-cookie:koa.sess=...,具体缘由不详。

此时,koa-session会自动设置添加cookies,具体内容如下:

4. 配置下一次请求,携带上cookies

我的前端和后端的域名都为localhost,但前后端端口不一致,所以客户端Axios需要配置withCredentials:true, 同时服务端也需要配置Access-Control-Allow-Credentials:true

具体配置内容如下:

服务端,使用koa-cors中间件,cors配置项如下: 此时,再次发起请求浏览器就会自动携带cookies。

  1. 模拟不同用户,请求携带不同的session_id(即koa.sess),查看返回的的用户信息

调试koa-session源码,查看获取ctx.session的逻辑

如上图,此时opts.keykoa.sess,从cookies中获取key为koa.sess的cookie。

上图中,body的值如下:body中除了用户信息外,还存有过期时间。

js 复制代码
'{"userInfo":{"id":"1716945823362292464170","user_name":"lily","is_admin":true},"_expire":1717032223363,"_maxAge":86400000}'

通过 Buffer.from(string, 'base64') 将base64编码的字符串转换为Buffer对象。 toString('utf8') 将Buffer对象转换为utf8编码的字符串。

由上面的源码解析可以的值,koa-session实现session_id的逻辑是将存储用户信息和过期时间的对象转化为对象字符串,再进行base64编码。

如上图,获取到用户信息和过期时间后,接着判断是否过期。

最后,将用户信息赋值给ctx.session。

由此可见,koa-session,默认是将用户信息base64编码后存储在cookie中。

session存放在cookie中的问题

  1. base64编码后的内容会随着原内容的增加而变长,而cookie的长度是有限制的。
  2. 如果cookie被攻击者盗取,通过base64解码,很容易获取到用户的信息。

koa-session实现session存储在内存

  1. 安装koa-session-memory
js 复制代码
pnpm install koa-session-memory
  1. 使用koa-session-memory
  1. 查看请求头中的cookie,此时koa-sess的值短了很多了。

4. 此时,内存中就有一个对象用来存储所有session。该对象就是cookie中key为koa-sess的值。也就是常说的session_id。

Token验证

JWT

可用来生成验证token

JWT三个组成部分:

  • header 头部
  • playload 载荷
  • signatrue 签名,保证令牌的安全性

Header部分是一个JSON对象,描述JWT的元数据,通常是下面这个样子。

js 复制代码
{
    "alg": "HS256",
    "typ": "JWT"
}

上面代码中,alg属性表示签名的算法,默认是HMAC SHA256(写成HS256);typ属性表示这个令牌(token)的类型,JWT令牌统一写为JWT。

最后,将上面的JSON对象使用Base64URL算法(转成字符串)。

Payload

Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。

  • iss (issuer): 签发人
  • exp (expiration time): 过期时间
  • sub (subject): 主题
  • aud (audience): 受众
  • ndf (Not Before): 生效时间
  • iat (Issued At): 签发时间
  • jti (JWT ID): 编号

除了官方字段,你可以在这个部分定义私有字段,下面就是一个例子。

js 复制代码
{
    "id": "7832713263",
    "name": "lily",
    "admin": true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把密码信息放在这个部分。

这个JSON对象也要使用Base64URL算法转成字符串。

Signature

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄漏给用户。然后,使用Header里面指定的签名算法,按照下面的公式产生签名。

js 复制代码
HMACSHA256(
    base64UrlEncode(header) + "." + 
    base64UrlEncode(payload),
    secret
)

算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用"点"(.)分割,就可以返回给用户。

JWT的使用方式

客户端收到服务端返回的JWT,可以储存在Cookie里面,也可以存储在localStorage。

此后,客户端每次和服务器通信,都会带上这个JWT。你可以放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP请求的头信息Authorization字段里面。

js 复制代码
Authorization: Bearer <token>

另外一种做法是,跨域的时候,JWT就放在POST请求的数据体里面。

参考文档: www.ruanyifeng.com/blog/2018/0...

Token的缺点

  1. token太长了 token是header、payload编码后的样式,所以一般要比sessionId长很多,很有可能超出cookie的大小限制(cookie一般有大小限制,如4kb)。
  2. 不太安全 比如使用JWT实现的token,payload部分是用户信息,只是进行base64Url编码,默认并没有加密。所以token被攻击者窃取后,很容易解析出用户信息。

上面koa-session默认方式和token认证很相似,只是较JWT没有签名部分。

Node + Koa实现Token认证

  1. 安装jsonwebtoken
js 复制代码
pnpm install jsonwebtoken
  1. 用户登录后,服务器通过jsonwebtoken生成token,并返回给客户端
js 复制代码
const jwt = require('jsonwebtoken')

 const info = {
            id: uuid,
            user_name: user_name,
            is_admin: true
        }
        
jwt.sign(info, JWT_SECRET, {
                    expiresIn: '1d' //过期时间失败
                }) 
  1. 客户端再次请求时,将token放在请求头中,通常是将token放在一个名为Authorization的请求头中。

token前面的Bearer是什么?

developer.mozilla.org/zh-CN/docs/...

4. 服务端接受新的接口,会从请求头中拿到token,验证该token是否有效,是否过期。

js 复制代码
const jwt = require('jsonwebtoken')
const { tokenExpiredError, invalidToken  } = require('../constant/err.type')
const auth = async (ctx, next) =>  {
    const { authorization} = ctx.request.header
    const token = authorization.replace('Bearer ', '')
    const  JWT_SECRET = 'abcde' //密钥
    try {
    // user中包含payload信息(ID, user_name, is_admin)
       const user =  jwt.verify(token, JWT_SECRET)
    } catch (error) {
        // token过期
        // token无效
        console.error(11, error.name)

        switch(error.name) {
            case 'TokenExpiredError': 
                console.error('token 已过期', error)
                return ctx.app.emit('error', tokenExpiredError, ctx)
            case 'JsonWebTokenError':
                console.log('无效的token', error)
                return ctx.app.emit('error', invalidToken, ctx)
        }
    }
    await next()
}
相关推荐
长天一色2 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_23419 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河22 分钟前
CSS总结
前端·css
BigYe程普44 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297912 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_2 小时前
meta标签作用/SEO优化
前端·javascript·html