关于学习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地址等......

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/852072.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

韩顺平0基础学java——第22天

p441-459 异常exception 选中代码块&#xff0c;快捷键ctraltt6&#xff0c;即trt-catch 如果进行了异常处理&#xff0c;那么即使出现了异常&#xff0c;但是会继续执行 程序过程中发生的异常事件分为两大类&#xff1a; 异常体系图※ 常见的运行异常&#xff1a;类型转换…

vs2019 c++20规范 STL 库中头文件 <atomic> 源码注释及探讨几个知识点

&#xff08;1 探讨一&#xff09; 模板类 atomic 的继承关系与数据结构如下&#xff1a; (2 探讨二 ) 可见 atomic 的 fetch_xx 函数&#xff0c;返回的都是 atomic 中存储的旧值。测试如下&#xff1a; 谢谢

【MySQL】mysql中常见的内置函数(日期、字符串、数学函数)

文章目录 案例表日期函数字符串函数数学函数其他函数 案例表 emp students 表 exam_result 表 日期函数 注意current_time和now的区别 案例一&#xff1a; 创建一张表用来记录生日&#xff0c;表结构如下 添加日期&#xff1a; insert tmp (birthday) values (2003-01-3…

永磁同步电机滞环电流控制(PI双闭环)matlab仿真模型

微♥“电击小子程高兴的MATLAB小屋”获取模型 1.滞环电流控制的原理 将给定的电流信号与反馈的电流信号进行比较&#xff0c;然后控制它俩之间的差值稳定在一个滞环范围内&#xff0c;若超出范围&#xff0c;则进行相应的调节操作。 操作如下叙述&#xff1a;假设以三相中的A相…

unDraw —— 免费且可定制的插画库,为您的设计注入灵魂

&#x1f3a8; unDraw —— 免费且可定制的插画库&#xff0c;为您的设计注入灵魂 在寻找能够完美融入您品牌风格的插画吗&#xff1f;unDraw&#xff0c;一个提供大量免费插画资源的网站&#xff0c;可能是您的理想选择&#xff01; &#x1f310; 网站特色 免费且开源 unDraw…

项目太大导致报错:JavaScript堆内存已满

1.问题 启动一个Vue项目的时候遇到了如下的报错 Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 无效的标记压缩导致接近了堆上限&#xff0c;分配失败 - JavaScript内存不足 2.解决方法 我查阅了网上的资料&#xff0c;似乎…

udp协议下的socket函数

目录 1.网络协议 2.网络字节序 3.socket编译接口 4.sockaddr结构体 5.模拟实现 1.socket函数 2.bind函数&#xff08;绑定&#xff09; 1.讲解 1.如何快速的将 整数ip<->字符串 2.ip地址的注意事项 3.端口号的注意事项 3.recvfrom函数 4.sendto函数 5.代码呈…

不测评不知道,该这款主食冻干嚣张!PR、希喂、扑呀真实测评

主食冻干喂养越来越火了&#xff0c;除了知名的“四大金刚”K9、VE、SC、PR之外&#xff0c;也有像希喂、扑呀这类以营养、高肉含量为切入点的新锐品牌&#xff0c;各大猫粮商更是纷纷推出了自家的主食冻干产品。目前关于主食冻干的讨论也很多&#xff0c;但大多数还是以科普和…

活久见!谁想的这种办法让大模型PK

文&#xff5c;白 鸽 编&#xff5c;王一粟 “每个大模型看起来都差不多&#xff0c;只能谁便宜先用谁的。但用下来之后&#xff0c;不合适再换&#xff0c;又费钱又费力”&#xff0c;一位AI 招聘公司的创始人对光锥智能抱怨道。 2024年&#xff0c;大模型正在加速走向行…

【Apollo配置中心】集成springboot自动监听属性变更和动态发布配置

1. 背景 在实际项目中&#xff0c;Spring Boot项目结合使用Apollo配置中心时&#xff0c;经常会遇到需要更新Apollo上的项目的一些配置&#xff0c;比如测试环境或生产环境中&#xff0c;需要修改某个类的属性值&#xff0c;如果我们在Apollo上更新了配置&#xff0c;已经在运…

因数与倍数 初级题目

最近又来更题了。这一次是《第三单元 因数与倍数第一部分》的初级题目。 参考答案见文尾 参考答案&#xff1a; CBDAABCBBACCCCCBCDCC

3389端口修改工具,修改3389端口的操作

3389端口作为远程桌面协议&#xff08;RDP&#xff09;的默认端口&#xff0c;常常成为黑客攻击的目标。为了提高系统的安全性&#xff0c;修改3389端口成为一项重要的安全措施。本文将详细介绍如何使用3389端口修改工具进行专业操作&#xff0c;以确保系统的安全稳定。 一、备…

计算机网络(3) 字节顺序:网络字节序与IPv4

一.小端与大端 小端&#xff08;Little endian&#xff09;&#xff1a;低字节保存在内存低地址&#xff0c;高字节保存在内存高地址。 大端&#xff08;Big endian&#xff09;&#xff1a;低字节保存在内存高地址&#xff0c;高字节保存在内存低地址。 例如&#xff08;14…

Python私教张大鹏 Vue3整合AntDesignVue之DatePicker 日期选择框

案例&#xff1a;选择日期 <script setup> import {ref} from "vue";const date ref(null) </script> <template><div class"p-8 bg-indigo-50 text-center"><a-date-picker v-model:value"date"/><a-divide…

Day50 代码随想录打卡|二叉树篇---验证二叉搜索树

题目&#xff08;leecode T98&#xff09;&#xff1a; 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右…

unity开发Hololens编辑器运行 按空格没有手

选择DictationMixedRealityInputSystemProfile 如果自定义配置文件 需要可能需要手动设置 手部模型和材质球

Centos: ifconfig command not found且ip addr查不到服务器IP

前段时间部门新派发了服务器&#xff0c;让我过去使用U盘装机&#xff0c;装完后使用ifconfig查不到服务器IP地址&#xff0c;ip addr也是查不到 ifconfig&#xff1a;command not found (这两个图片先用虚拟机的替代一下) 在网上找资料(CSDN&#xff0c;博客园&#xff0c;知乎…

使用 Vue 和 Ant Design 实现抽屉效果的模块折叠功能

功能描述&#xff1a; 有两个模块&#xff0c;点击上面模块的收起按钮时&#xff0c;上面的模块可以折叠&#xff0c;下面的模块随之扩展 代码实现&#xff1a; 我们在 Vue 组件中定义两个模块的布局和状态管理&#xff1a; const scrollTableY ref(560); // 表格初始高度…

分类模型:MATLAB判别分析

1. 判别分析简介 判别分析&#xff08;Discriminant Analysis&#xff09; 是一种统计方法&#xff0c;用于在已知分类的样本中构建分类器&#xff0c;并根据特征变量对未知类别的样本进行分类。常见的判别分析方法包括线性判别分析&#xff08;Linear Discriminant Analysis, …

人工智能的潜在威胁:罗曼·扬波尔斯基对AGI的警示

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术正迅速成为人类社会不可或缺的一部分。然而&#xff0c;随着人工智能技术的发展&#xff0c;一些科学家对其潜在的危险表示了担忧。本文将深入探讨计算机科学家罗曼扬波尔斯基对人工智能特别是人工通用智…