6

当前项目JWT登录鉴权方法及问题

当前登陆鉴权方法

用户登录时,返回一个JWT,由前端存储在本地。此后用户每次向需要权限的API发出请求时带上该JWT用于验证身份。
后端收到JWT后,验证JWT是否正确且未过期:

  1. 如果正确且未过期,则返回资源。
  2. 如果不正确,则返回不正确的状态码;如果已过期,则返回已过期的状态码,要求重新登录。

问题

使用JWT实现登录鉴权时,由于JWT有固定的过期时间,可能在用户正在进行操作的过程中恰好过期,然后突然跳转到登录界面,造成较差的用户体验。

目标

使用JWT实现登录鉴权,当且仅当用户在一段时间内不对系统进行任何操作时才要求用户重新登录,并且尽可能保证JWT的安全性。

解决方法及优缺点分析

解决方法一:只使用一个JWT token(不推荐)

具体实现

用户登录时,返回一个JWT,由前端存储在本地。此后用户每次向需要权限的API发出请求时带上该JWT用于验证身份。
后端收到JWT后,验证该JWT是否正确:

  1. 如果不正确,则直接返回错误代码;
  2. 如果正确,则验证该JWT是否过期:

    • 如果没有过期,则直接返回资源;
    • 如果过期,则计算过期时间:

      • 如果过期时间没有超过某阈值,则返回资源和新的JWT;
      • 如果过期时间超过某阈值,则返回错误代码。要求重新登录。
优点

实现了当且仅当用户在一段时间内不对系统进行任何操作时才要求用户重新登录。
当JWT过期时如果用户还在操作系统,会向后端发送刚刚过期的JWT,此时JWT的过期时间没有超过阈值,直接返回资源和新的JWT,实现用户无感书最新。
但是如果当JWT过期之后的一段时间(阈值内)用户都没有操作系统,当用户再次操作系统时,会向后端发送过期时间超过阈值的JWT,后端会返回错误代码,实现重新登录。

缺点

安全性较低。如果频繁发送请求,可以使用一个JWT实现永久登录。一旦JWT被窃取,攻击者可以使用得到的JWT永久伪造用户获取信息。

解决方案二:使用两个JWT token(推荐)

具体实现

用户登录时,返回两个JWT(access token和refresh token),一个用于在请求资源验证身份的access token,一个用于在access token过期时更新access token的refresh token。其中refresh token的有效期较长(如7天),而access token的有效期较短(如1小时)。由前端存储在本地。此后用户每次向需要权限的API发出请求时带上access token用于验证身份。
后端收到access token后:验证该access token是否正确:

  1. 如果不正确,则直接返回错误代码;
  2. 如果正确,则验证该access token是否过期:

    • 如果没过期,则直接返回资源;
    • 如果过期,则返回过期的错误代码。前端收到过期错误代码后使用refresh token向更新接口请求新的access token,更新接口判断refresh token是否过期:

      • 如果没有过期,则返回新的access token和新的refresh token,将之前所有由同一refresh token生成的refresh token都置为无效(在更新接口维护一个无效列表)。前端使用新的access token重新向需要权限的API发出请求,获取资源。
      • 如果过期,则返回错误代码,要求重新登录。
      • 如果收到已被置为无效的refresh token,则将当前所有与无效refresh token为同一用户的refresh token和access token都置为无效(具体来说是将该用户的访问置为无效),直到用户重新登录。

使用这种方法,既能避免在用户操作的过程中出现突然跳转登录(主要解决了两次操作之间的不一致现象,不会出现一次请求还能正常获取资源,下一次请求资源就突然跳转到登录页面的现象),又能保证较高的安全性。

解释
Q:为什么不使用永久有效的refresh token?

A:为了实现更高的安全性。永久有效的refresh token一旦被窃取,攻击者可以使用该token永久冒充用户获取个人信息。而每次使用refresh token就更换一个新的refresh token的话,可以通过更新接口返回新refresh token的同时将旧refresh token置为无效的做法,避免攻击者使用窃取的refresh token获取新的refresh token/access token。

Q:为什么refresh token获取新的access token时需要同时更新refresh token?

A:因为如果不更新refresh token的话,就无法避免用户两次操作之间的不一致性。可能用户在refresh token过期前一分钟还能正常操作,一分钟后refresh token和access token同时过期,就突然跳转到登录页面,造成不友好的用户体验。

Q:为什么要在使用refresh token获得新的access token和refresh token后将用于请求新token的refresh token置为无效?

A:为了避免之前的refresh token被窃取后导致的重放攻击(使用之前的refresh token请求新的access token和refresh token)。

Q:为什么更新接口在收到已置为无效的refresh token后会将所有使用第一个refresh token得到的refresh token都禁止?

A:因为不知道被窃取的是哪个refresh token。有可能攻击者使用第一个refresh token,并使用该token在更新接口获取了新的refresh token,此时用户可能使用旧的refresh token(已被置为无效)请求更新接口。也有可能攻击者窃取到第一个refresh token后,用户首先使用该token在更新接口获取了新的refresh token,此时攻击者可能使用旧的refresh token(已置为无效)请求更新接口。详见:auth0-refresh-token-rotation

Q:收到无效refresh token时,如何将所有与无效refresh token为同一用户的refresh token和access token都置为无效?

A:

  1. 缩短JWT的有效时间,然后在更新接口维护一个用户列表,禁止所有该用户的refresh token:但是access token最短也有一个固定的有效期限(5~10min),可能给攻击者提供5~10min使用该token的时间。
  2. 维护一个禁止访问的用户列表,每次校验JWT时判断是否是禁止用户的JWT,直到用户重新登录:当一个refresh token被禁止时将该消息广播到所有有关的服务器,所有的服务器维护一个收到无效refresh token的用户列表,每次接到一个access token或refresh token时,判断其对应的用户是否在列表上,如果是就直接返回错误状态码。这样在用户重新登录之前,不能使用尚未过期的access token访问任何业务接口,也不能使用尚未过期的refresh token获取新的access token。
  3. 详见:fusionauth-revoking-jwts

JWT VS Session:登录鉴权如何选

二者都是为了在无状态的HTTP协议中记录登录状态,以避免每次请求都要验证身份。

Session实现登录鉴权

用户登录成功后,服务器生成一个对应的session ID,存储在(内存里的)文件或数据库中,并且放在cookie中返回给浏览器。登录后每次请求会发送cookie,在服务端的存储中查找cookie中的session ID以验证用户身份。

对比

  1. (水平)扩展性:当用户量增大时,需要使用多台服务器处理访问。

    • 如果使用session,需要解决服务器端存储session ID的问题,因此需要一个集中的session存储器,一般是一台专门用于运行redis的服务器。但是这样也会面临访问量瓶颈的问题。并且同时在线人数过多时需要存储大量session ID,从而占用大量服务器资源。
    • 如果使用JWT,就不用在服务端存储每个用户的登录信息(除了还未过期但被禁用的refresh token,但是不用在用户请求业务接口时验证,只用在更新token时验证,不会影响业务接口的响应时间)。任何一台服务器都能直接通过JWT判断是否是合法的登录用户。
  2. 性能:如果JWT中包含了很多数据,则它会远大于一个session ID,会给HTTP请求带来额外的开销。但是session每次都要查找一次数据库来验证用户身份。
  3. 在服务端废除一个用户当前所有有效的身份验证信息(如用户修改密码后禁止使用原来的信息访问业务接口):

    • 如果使用session:比较简单,唯一的身份验证信息是session ID,因此直接从数据库中删除session ID即可。
    • 如果使用JWT:比较复杂。需要废除一个用户所有尚在有效期内的access token和refresh token。而token一旦签发,在有效期内都能被服务器认证,因此需要维护一个被禁用用户的blacklist,请求业务接口/更新access token都要查找,如果对应的用户在该blacklist上,则拒绝返回业务数据/新的access token。
  4. 如何退出登录:

    • 如果使用session:删除session ID。
    • 如果使用JWT:一般的做法是直接将客户端存储(cookie或localstorage)的JWT从客户端删除。但是登出后,使用上次登录中尚未过期的JWT仍能访问业务接口/更新接口。如果要在服务器端实现真正完全的退出登录,请参考3。

参考


MarsTokio
43 声望1 粉丝

引用和评论

0 条评论