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.key为koa.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()
}
相关推荐
剪刀石头布啊6 分钟前
泛洪DFS、BFS
前端
qetfw6 分钟前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊6 分钟前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊12 分钟前
gird网格布局
前端
啃火龙果的兔子13 分钟前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊16 分钟前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊21 分钟前
js中改变this指向的操作有哪些
前端
剪刀石头布啊21 分钟前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛22 分钟前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架
java_nnnn1 小时前
JavaEE进阶-HTML基础认识
前端·java-ee·html