关于学习Token、JWT、Cookie等验证授权方式的总结

2024-06-14 02:44

本文主要是介绍关于学习Token、JWT、Cookie等验证授权方式的总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、为什么Cookie无法防止CSRF攻击,而Token可以?

二、为什么无论采用Cookie-session的方式,还是Token(JWT)的方式,在一个浏览器里,同一个网站只能保证一个用户处于登录状态?

三、理解黑马点评中的双重拦截器

四、什么是ThreadLocal?底层是如何实现的

4.1 ThreadLocal为什么可以优化鉴权逻辑?

 4.2 ThreadLocal内存泄漏的原因

4.2.1 那为什么不将key设置为强引用?

4.2.2 那么为什么 key 要设计为弱引用 ?

4.3 如何正确使用ThreadLocal?

五、JWT身份认证常见问题及解决方法

5.1 JWT概念

5.2 如何防止JWT被篡改?

5.3 如何加强JWT的安全性?

5.4 JWT身份认证常见问题及解决方案

5.4.1 JWT续签问题

5.4.2 JWT实现强制某用户只能在一种客户端登录怎么实现?


最近接触了许许多多的项目,对这些概念有些许混淆,趁着这次温书假,对其进行巩固一下。

在此之前,我们先讲讲传统的关于Cookie-session方案:

很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:

  1. 用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie。
  2. 当用户向后端发起请求的时候会把SessionID 带上,这样后端就知道你的身份状态了。

关于这种认证方式更详细的过程如下:

  1. 用户向服务器发送用户名、密码、验证码用于登陆系统。
  2. 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储起来。
  3. 服务器向用户返回一个 SessionID ,写入用户的 Cookie。
  4. 当用户保持登录状态时,Cookie将与每个后续请求一起被发送出去。
  5. 服务器可以将存储在 Cookie上的 SessionID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。

使用 Session 的时候需要注意下面几个点:

  • 依赖 Session 的关键业务一定要确保客户端开启了 Cookie。(当然,没用开启可以使用URL重写技术进行传递SessionID)
  • 注意 Session 的过期时间。

一、为什么Cookie无法防止CSRF攻击,而Token可以?

CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造 。那么什么是 跨站请求伪造 呢?说简单点,就是用你的身份去发送一些对你不友好的请求。举个简单的例子:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科学理财,年盈利率过万</>

上面也提到过,进行 Session 认证的时候,我们一般使用 Cookie 来存储 SessionID ,当我们登陆后后端生成一个 SessionID 放在 Cookie 中返回给客户端,服务端通过 Redis 或者其他存储工具记录保存着这个 SessionID ,客户端登录以后每次请求都会带上这个 SessionID ,服务端通过这个 SessionID 来标示你这个人。如果别人通过 Cookie 拿到了 SessionID 后就可以代替你的身份访问系统了。

Session认证中 Cookie 中的 SessionID 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,我们使用 Token 的话就不会存在这个问题,在我们登录成功获得 Token 之后,一般会选择存放在localStorage(浏览器本地存储)中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个Token ,这样就不会出现 CSRF 漏洞的问题。因为,即使你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 Token 的,所以这个请求将是非法的。

需要注意的是:不论是 Cookie 还是 Token 都无法避免 跨站脚本攻击(Cross Site Scripting)XSS

跨站脚本攻击(Cross Site Scripting)缩写为 CSS 但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为 XSS。

XSS 中攻击者会用各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本盗用信息比如 Cookie 。

二、为什么无论采用Cookie-session的方式,还是Token(JWT)的方式,在一个浏览器里,同一个网站只能保证一个用户处于登录状态?

其实这里主要是前端的设置,因为一般前端才向后端发送请求的时候,基本是拿去浏览器所存储的JSESSIONID和Token,如果硬要保证,一个浏览器同一个网站可以保持多个登录为登录状态的话,设计较为麻烦。

其实像黑马点评里面的登录校验逻辑,其实也是类似于JWT一样,感觉JWT只是一种较为正规的Token,多封装了一些签名,负载等。

三、理解黑马点评中的双重拦截器

在初始方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案是存在问题的:

优化方案

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

四、什么是ThreadLocal?底层是如何实现的

其实简单来讲,ThreadLocal就是线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。

ThreadLocal底层是使用ThreadLocalMap 实现的,这点从JDK的源码可以看出,核心源码如下:

从ThreadLocal的Set方法可以看出,ThreadLocal是存储在ThreadLocalMap中的,咱们继续看ThreadLocalMap的源码实现:

从上面源码可以看出,ThreadLocalMap中存放的是Entry,而Entry中的key就是ThreadLocal,而Value则是要存储的值,所以我们得出ThreadLocal的实现如下所示:

4.1 ThreadLocal为什么可以优化鉴权逻辑?

我们先来看这幅图:当我们访问这个程序的时候,所有的请求,其实是需要先通过拦截器的,但是在拦截器之后呢?那些Contorller层是怎么知道当前用户信息的呢?

这时候我们就可以将其存到ThreadLocal中了。

可能有人会说,这不是将用户信息存入Sesssion中吗?我每次都去Session中取不可以吗?

  • 这是因为使用ThreadLocal可以简化代码,使得当前线程可以随时的获取当前用户的信息,而不必每次都通过‘HttpServletRequest’来获取。
  • 并且可以避免的频繁访问Session,提高性能。

使用ThreadLocal优化也是较为常见的操作,上图中的情况可以通过Session获取,但是在分布式架构中,可能使用的是Token/JWT结合Redis进行存储用户信息,用户信息都放在Redis中,如果每次获取用户信息都需要去查询Redis,那么就太浪费性能了。所以使用ThreadLocal成为一种常见的方式。

可能有人说,那为什么网上有些文章说ThreadLocal可以避免线程安全问题?

其实这是从另一个维度来区分的,这个结论有个前提条件,是相比于普通的变量,使用ThreadLocal可以有效的避免线程安全问题。

下面我们来看这个例子:(以下原文为:为什么要使用 ThreadLocal 进行登录时处理用户信息?而非普通变量?_threadlocalcontext 在登录的时候-CSDN博客)

假如有两个用户 A 和 B,他们分别进行登录,并且他们的每次请求都会带有自己的 token,在请求到达 controller 之前(preHandle() 中),每次都会被会被拦截器进行拦截,提取出当前 token 中的用户信息(比如 userId),认证通过以后在 service 中就可以通过 Contenxt 类获取提取出来的用户信息:

前提

  • Context 类中存储用户的 ID,有一个静态的变量或者对象叫做 USER_ID

(1)使用普通变量,例如 String

Context 类使用 String 变量 (static String USER_ID) 进行存储,这时候:

  • 用户 A 登录,带着自己的 token,到达后端拦截器,token 验证通过后,用户信息被提取到 Context 中的 String 变量中。
  • 用户 B 登录,带着自己的 token,到达后端拦截器,token 验证通过后,用户信息被提取到 Context 中的 String 变量中。(这里 String 变量之前存储的是 A 的信息,但是由于 B 登录以后,又将 String 的值设置为了 B 的 token 中提取出来的用户信息。)
  • 用户 A 调用新增的 api,这时候调用新增 api 的这个请求,也附带了 A 的 token 信息,所以重复第一步。
  • 用户 B 调用新增的 api,这时候调用新增 api 的这个请求,也附带了 B 的 token 信息,所以重复第二步。

虽然存储用户信息都是在一个 String 中,但是好像并没有发现什么问题。(往下看)

(2)使用ThreadLocal类进行存储

Context 类使用 ThreadLocal 类型的对象 (ThreadLocal USER_ID = new …) 进行存储,这时候:

  • 用户 A 登录,带着自己的 token,到达后端拦截器,token 验证通过后,接下来应该提取用户信息到 Context 中了,这时候,用户 A 当前登录是在一个线程 ThreadA 中,那么看到 Context 中定义的 USER_ID 是 ThreadLocal 类型的,(简单讲)这时候他会以当前线程为 key = ThreadA,以 A.token 为 value 创建一个新的只属于当前 ThreadA 的对象 USER_ID。
  • 用户 B 登录,带着自己的 token,到达后端拦截器,token 验证通过后,接下来应该提取用户信息到 Context 中了,这时候,用户 B 当前登录是在一个线程 ThreadB 中,那么看到 Context 中定义的 USER_ID 是 ThreadLocal 类型的,(简单讲)这时候他会以当前线程为 key= ThreadB,以 B.token 为 value 创建一个新的只属于当前 ThreadB 的对象 USER_ID。

总结

第一种方式看似运行时和第二种没有区别,但是在高并发的时候,由于 USER_ID 的值的设置和 USER_ID 的值的获取是两次操作,那么很显然设置和获取不是一个原子性的操作,这时候肯定会发生并发问题,即:A 刚设置了值,还没有等到 A 取值,B 就将这个 String 类型的 USER_ID 设置成了自己的信息。这时候 A 再进行取值,取到的就是 B 的值。
第二种方式的话,很显然就解决了这个问题,因为他们都是操作的自己线程内的 USRE_ID,各个线程之间互不影响,所以这个时候,完全不会混乱。

注意

  • 首先说一个名词 OOM,即 Out Of Memory,内存泄露、内存溢出。
  • ThreadLocal 中的 key 是弱引用,value 是强引用。
  • 弱引用,自动垃圾回收。
  • 强引用,线程销毁时,才会被回收。
  • 一个线程可能有时候很久都不会被销毁,但是这时候只有弱引用的 key 会被回收,value 由于是强引用,由于线程还存在,他就会存在,但是 value 已经没有用了,这个时候就造成了浪费。
  • 为了避免浪费内存,继而发生内存溢出,我们需要使用 remove() 方法,进行手动清除 ThreadLocal 对象。

 4.2 ThreadLocal内存泄漏的原因

原文链接:史上最全ThreadLocal 详解(二)_史上最全threadlocal 详解(二)-CSDN博客 

 Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。

主要是由于两个原因:

1. 没有手动删除这个Entry

2. CurrentThread即当前线程仍然运行

  • 第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
  • 第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。

综上, ThreadLocal 内存泄漏的根源是:

    由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。

4.2.1 那为什么不将key设置为强引用?

1.key如果为强引用

其实很简单,如果key被设计成强引用,且没有手动remove(),那么key会和value一样伴随着线程的整个生命周期。

如下图,如果栈上的引用没有了,堆上的两个强引用还存在,没有手动的remove,那么着两个强引用就一直没办法进行垃圾回收,这种设计仍然存在内存泄漏问题。

4.2.2 那么为什么 key 要设计为弱引用 ?

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障。——这便是ThreadLocalMap的自我清理机制。

但是即使如此,还说存在一些情况无法覆盖: 

  • 如果没有频繁访问ThreadLocal 的get,set 或 remove 方法,自我清理机制不会被触发,垃圾回收器不会回收那些强引用的值对象。

4.3 如何正确使用ThreadLocal?

  1.  将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
  2.  每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

关于第一点,可能有人会有点疑惑,这里我们展开介绍:

ThreadLocal的目的是为每个线程提供一个独立的变量副本,如果‘ThreadLocal’变量是‘private static’的,则它在类级别上是共享的,但每个线程对它的访问是独立的,这意味着每个线程在访问ThreadLocal变量时,实际上访问的是自己独有的变量副本,这种设计符合‘ThreadLocal’的初衷,即为每个线程提供独立的、隔离的变量副本。

五、JWT身份认证常见问题及解决方法

5.1 JWT概念

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则

并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

JWT的构成

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分: 

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
  • Payload : 用来存放实际需要传递的数据
  • Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

JWT 通常是这样的:xxxxxxx.yyyyyy.zzzzzz

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以在 jwt.ioopen in new window 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。

Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

Header 通常由两部分组成:

  • typ(Type):令牌类型,也就是 JWT。
  • alg(Algorithm):签名算法,比如 HS256。
{"alg": "HS256","typ": "JWT"
}

JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。

Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。

Claims 分为三种类型:

  • Registered Claims(注册声明):预定义的一些声明,建议使用,但不是强制性的。
  • Public Claims(公有声明):JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registryopen in new window 中定义它们。
  • Private Claims(私有声明):JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。

下面是一些常见的注册声明:

  • iss(issuer):JWT 签发方。
  • iat(issued at time):JWT 签发时间。
  • sub(subject):JWT 主题。
  • aud(audience):JWT 接收方。
  • exp(expiration time):JWT 的过期时间。
  • nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti(JWT ID):JWT 唯一标识。
{"uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a","sub": "1234567890","name": "John Doe","exp": 15323232,"iat": 1516239022,"scope": ["admin", "user"]
}

Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。

Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。

这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥(一定不要泄露出去)。
  • 签名算法。

签名的计算公式如下:

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是 JWT 。

5.2 如何防止JWT被篡改?

有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature、Header、Payload。

这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。

不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。

密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。

5.3 如何加强JWT的安全性?

  1. 使用安全系数高的加密算法。
  2. 使用成熟的开源库,没必要造轮子。
  3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
  4. 一定不要将隐私信息存放在 Payload 当中。
  5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
  6. Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。
  7. ...........

5.4 JWT身份认证常见问题及解决方案

注销登录等场景下JWT还有效

与之类似的具体相关场景有:

  • 退出登录;
  • 修改密码;
  • 服务端修改了某个用户具有的权限或者角色;
  • 用户的帐户被封禁/删除;
  • 用户被服务端强制注销;
  • 用户被踢下线;
  • .........

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。

那我们如何解决这个问题呢?查阅了很多资料,我简单总结了下面 3 种方案:

1、将 JWT 存入数据库

将有效的 JWT 存入数据库中,更建议使用内存数据库比如 Redis。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 都要先从 Redis 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。

2、黑名单机制

和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。

前两种方案的核心在于将有效的 JWT 存储起来或者将指定的 JWT 拉入黑名单。

虽然这两种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案。

3、保持令牌的有效期限短并经常轮换

很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。

另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

5.4.1 JWT续签问题

JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。

JWT 认证的话,我们应该如何解决续签问题呢?查阅了很多资料,我简单总结了下面 4 种方案:

1、类似于 Session 认证中的做法(不推荐)

这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。

2、每次请求都返回新 JWT(不推荐)

这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。

3、JWT 有效期设置到半夜(不推荐)

这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统

4、用户登录返回两个 JWT(推荐)

借鉴OAuth2.0的方法,第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。refreshJWT 只用来获取 accessJWT,不容易被泄露。

客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。

这种方案的不足是:

  • 需要客户端来配合;
  • 用户注销的时候需要同时保证两个 JWT 都无效;
  • 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT);
  • 存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。不过,由于 refreshJWT 只用来获取 accessJWT,不容易被泄露。

对于第四点,这里展开进行分析,对于续期问题,可能有人会说,使用单个JWT+redis+自动续期这种方案来实现,但是   单token+redis+自动续期的缺点:

单token设置短期的话,虽然一直操作可以通过拦截器重置token过期时间让它续期,但是如果隔一会儿不操作不续期,超过过期时间就过期了,那么用户需要重新登录,体验不好。

单token设置长期的话,就会有被盗用的风险。

如果是自动续期还是同一个token,那么token过期时间延长变成长期token那么还是会有盗用的风险。

如果是自动续期的时候刷新token,那么是拦截器是否需要返回新的token给前端,重新发起请求?(长期的自动续期有盗用风险,短期的自动续期如果隔会儿不操作还是会有重新登录的问题)

那么有没有一种实现方式既能活跃用户长时间登录,token还能安全不被盗用呢?

引入refresh token,就解决了【access token设置时间比较长,容易泄露造成安全问题,设置时间比较短,又需要频繁让用户授权】的矛盾。

使用双token无感刷新:

双token:一个短期token,一个长期token。

短期token为访问token。

长期token为refreshToken。当短期token过期,前端发起请求刷新token接口入参为refreshToken,用来获取新的短期token,同时获取新的refreshToken返回给前端。(刷新token和refreshToken都是为了防止两种token永不过期并且一直重复会被盗用)

刷新token接口中处理:校验refreshToken是否存在redis中,如果存在则刷新token和refreshToken返回前端(短期token刷新token和过期时间。长期token刷新token,是否刷新过期时间(续期)呢?)。前端用新的token访问。

如果不存在则返回未登录,让前端转去登录。

关于refreshToken是否续期的问题:

如果不续期,那么最终refreshToken会过期,可能导致用户正在操作的时候,强制重新登录。

如果续期,用户经常活跃的话,accessToken和refreshToken都会进行续期,用户可以一直在线。不会被强制重新登录。

当用户长时间不活跃的话,即refreshToken长时间不刷新过期时间,下次访问就会在refreshToken过期时,强制重新登录。

总结:

越频繁传输的越有风险,对于这种有风险的,要么避免频繁传输,如果不能避免就减少有效时间。

  • access_token频繁传输的风险通过缩短有效时间来解决。
  • access_token有效期短可能导致用户体验不佳,通过refresh_token解决。
  • refresh_token本身使用频率低,所以有效期长点也可以。

5.4.2 JWT实现强制某用户只能在一种客户端登录怎么实现?

案例描述:

如果一个用户在手机A中登录了,然后又在手机B中登录,怎么做到B登录后让A过期?

这个其实蛮好实现的,我简单总结了下面 3 种方案:

  1. 可以在JWT中的payload中记录ID,在服务端给客户端签发token的时候,后端记录该JWT的ID和JWT本身,将其看作一个Map,客户端发送请求时候,服务端验证是否存在该记录,有记录的话,将其删除即可(手机A在之后的访问中就会发现JWT失效,退出登录),然后再根据新的id,签发给客户端新的JWT即可。
  2. 签名的时候加上时间戳或者其他随机数,一旦登录就更新签名,之前的签名就失效了,这样就不会出现多个设备在线了。
  3. 或者登录时候通过记录设备信息、IP地址等......

这篇关于关于学习Token、JWT、Cookie等验证授权方式的总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1059152

相关文章

关于C++中的虚拟继承的一些总结(虚拟继承,覆盖,派生,隐藏)

1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下: class A class B1:public virtual A; class B2:pu

如何突破底层思维方式的牢笼

我始终认为,牛人和普通人的根本区别在于思维方式的不同,而非知识多少、阅历多少。 在这个世界上总有一帮神一样的人物存在。就像读到的那句话:“人类就像是一条历史长河中的鱼,只有某几条鱼跳出河面,看到世界的法则,但是却无法改变,当那几条鱼中有跳上岸,进化了,改变河道流向,那样才能改变法则。”  最近一段时间一直在不断寻在内心的东西,同时也在不断的去反省和否定自己的一些思维模式,尝试重

51单片机学习记录———定时器

文章目录 前言一、定时器介绍二、STC89C52定时器资源三、定时器框图四、定时器模式五、定时器相关寄存器六、定时器练习 前言 一个学习嵌入式的小白~ 有问题评论区或私信指出~ 提示:以下是本篇文章正文内容,下面案例可供参考 一、定时器介绍 定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。 定时器作用: 1.用于计数系统,可

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

[word] word设置上标快捷键 #学习方法#其他#媒体

word设置上标快捷键 办公中,少不了使用word,这个是大家必备的软件,今天给大家分享word设置上标快捷键,希望在办公中能帮到您! 1、添加上标 在录入一些公式,或者是化学产品时,需要添加上标内容,按下快捷键Ctrl+shift++就能将需要的内容设置为上标符号。 word设置上标快捷键的方法就是以上内容了,需要的小伙伴都可以试一试呢!

AssetBundle学习笔记

AssetBundle是unity自定义的资源格式,通过调用引擎的资源打包接口对资源进行打包成.assetbundle格式的资源包。本文介绍了AssetBundle的生成,使用,加载,卸载以及Unity资源更新的一个基本步骤。 目录 1.定义: 2.AssetBundle的生成: 1)设置AssetBundle包的属性——通过编辑器界面 补充:分组策略 2)调用引擎接口API

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

大学湖北中医药大学法医学试题及答案,分享几个实用搜题和学习工具 #微信#学习方法#职场发展

今天分享拥有拍照搜题、文字搜题、语音搜题、多重搜题等搜题模式,可以快速查找问题解析,加深对题目答案的理解。 1.快练题 这是一个网站 找题的网站海量题库,在线搜题,快速刷题~为您提供百万优质题库,直接搜索题库名称,支持多种刷题模式:顺序练习、语音听题、本地搜题、顺序阅读、模拟考试、组卷考试、赶快下载吧! 2.彩虹搜题 这是个老公众号了 支持手写输入,截图搜题,详细步骤,解题必备

idea lanyu方式激活

访问http://idea.lanyus.com/这个地址。根据提示将0.0.0.0 account.jetbrains.com添加到hosts文件中,hosts文件在C:\Windows\System32\drivers\etc目录下。点击获得注册码即可。

《offer来了》第二章学习笔记

1.集合 Java四种集合:List、Queue、Set和Map 1.1.List:可重复 有序的Collection ArrayList: 基于数组实现,增删慢,查询快,线程不安全 Vector: 基于数组实现,增删慢,查询快,线程安全 LinkedList: 基于双向链实现,增删快,查询慢,线程不安全 1.2.Queue:队列 ArrayBlockingQueue: