JWT验证

JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)

imgJWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

一、认证

  • 为了保存信息用的,除了认证信息还有些其他信息

cookie:在前端直接用cookie保存

session:用cookie在前端保存session_id,在后端用session_id保存内容

session在内存中,另外,cookies被获取的话,可能会跨站请求伪造攻击

  • 获取token会怎么样?不会跨站?
  • cookie网络传输时是明文吗?
  • 和cookie相比,是在服务器端验证了下 token是否存在么,,纯cookie模式就是cookie发啥我都信
    • 怎么验证?
    • 放在Authorization可以防止XSS和XSRF?

检查

  • 签名是否有效
  • 是否过期
  • 接受方是否是自己

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

  • 单点失败?

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT简介

原理

JWT 的原理是,服务器认证以后,生成一个字符串,发回给用户,就像下面这样。

img

它是一个很长的字符串,没有换行,中间用点(.)分隔成三个部分。

  • 怎么是json对象,方便前端使用么?话说这是认证用的,还指望用里面的信息?
  • 后端会再转换成json对象么?还是说一开始是个json对象,一开始这个怎么来的,为什么是少见的后端先生成JSON,因为后端先拿到数据么

以后,用户与服务端通信的时候,都要发回这个字符串。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

  • 无状态概念!

JWT 的三个部分依次如下

Header.Payload.Signature

元数据被定义为:描述数据的数据,对数据及信息资源的描述性信息。

Header

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

{
  "alg": "HS256",
  "typ": "JWT"
}

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

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

payload 负载

是一个JSON 对象, 用来存放实际需要传递的数据,形如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

其中payload官方规定了7个字段:

  • iss (issuer):签发人

  • exp (expiration time):过期时间

  • sub (subject):主题

  • aud (audience):受众

  • nbf (Not Before):生效时间

  • iat (Issued At):签发时间

  • jti (JWT ID):编号

除官方字段外,也可以直接使用私有字段(就像js对象的成员一样,毕竟是json数据),如之前例子中的admin

注意,JWT 默认是不加密的,不要把机密信息放在这个部分。Base64URL虽然人类难以看懂,但不是加密doge

  • 可逆么?不可逆是不是和md5类似

Signature

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

签名的生成:

需要指定一个密钥(secret,一般用随机盐),这个密钥只有服务器才知道。使用 Header 里面指定的签名算法,按照下面的公式产生签名:

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

这里以HMACSHA256加密算法为例

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

  • 为什么又把header和payload编码再加密?不能直接加密?只能从字符串加密?
  • 怎么解密?能解密的话还要前两部分干什么?

3.4 Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

Base64可以被解码,没有加密

JWT 的使用方式

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

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

Authorization: Bearer <token>

另一种做法是,放在 POST 请求的数据体里面。

cookie是和域名有关的,所以不能跨域

  • 安全机制把,原理是什么?后端人看见跨域cookie直接不用?
  • 自动发送?

JWT 的几个特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

  • 自包含:自己包含用户需要的信息,不需要查询数据库

验签:

服务端拿到jwt后,将其中的header和payload取出来,和自己服务端保存的secret放一起计算一次signature,如果计算出来的signature和之前保存的signature一样,则说明数据没有收到更改

如果中间人改了三部分中其中一项,将导致验签时计算出来的signature和之前不一样,无法认证。如果中间人修改了signature,直接就不用算就能知道被修改了。(发来的签名和现场计算的签名要和保存的签名一样。jwt认证用的,携带的信息不会在前端更改)

这就是为什么signature要header.payload.secret一起加密。我们需要header、payload中的信息,所以只是编码;而用于认证的加密则要三项信息都没有被修改。

  • 这是防修改的方式,和计网有点儿像?

  • 如果被人拿到了token假装身份,解决方式是超时淘汰?

  • 是不是安全要考虑防修改、防伪装、防查看?这个是传递的死数据,不超时就不会主动修改,那会主动修改的知道有没有被中间人修改?

  • 防查看这里倒是直接不写认证信息,,,要是cookie的话,不会要每次把密码发上去查查数据库把,,或者登录一次获取用户名cookie,然后谁拿着这个cookie就一直用?

  • 密码保存到cookie岂不是很麻烦?就算加密也只能对称吧,浏览器给他加个盐?

需求分析

前后端通信方式

  • 不能用cookie,跨域
  • 放在header的Authorization中
    • 需要传递编码后的字符串数据,能有+、换行吗?
      • 所以用base64url

目的:

  • 不敏感的用户信息能读取
    • 所以不能更改
    • 所以用base64url,可以还原信息
  • 认证的secret要加密,密钥不能让人读取
    • 要加密
    • 要保证所有数据不能被中间人更改,因为后端还要读取这些信息
      • 要一起加密

总结需求:

  • 数据要有合适的解析格式

  • 数据要有合适的传输格式,要用能跨域、防止攻击的传输方式

  • 要能从jwt读取用户非敏感信息,

    • 因此这部分数据不能被篡改
  • 不能从jwt读取密钥,所以传输时要加密

为什么不直接传输json、对json加密?为了方便么?都先json数据类型 为什么要用base64?因为数据类型、特殊符号吗?还是说加密算法没法直接加密json和这些特殊符号,所以原始数据要做处理?类似于加一层思想,屏蔽了特殊性?

但是处理的时候json更方便,虽然java创建时传参是<String, Objecr>map,

  • 这种map对应json?json是不是全字符串?还是说js数据类型?

话说java不用json包的话是不是经常用map代替,,都是键值对甚么区别,,

JWT验证对象

JWTVerifier,指定密钥和创建jwt时一样

常见异常

按触发顺序:

  • 算法不匹配异常
  • 签名不匹配异常
  • 过期异常
  • payload异常(payload数据不对劲儿,可能被修改)

可以看出是先验签再看数据

  • https不会被拦截?

springboot使用JWT

封装成工具类

这里是ruoyi-cloud中的jwt工具类,省略了导包

import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.TokenConstants;
import com.ruoyi.common.core.text.Convert;
import io.jsonwebtoken.Jwts;


public class JwtUtils
{
    public static String secret = TokenConstants.SECRET;

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token)
    {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 根据令牌获取用户标识
     * 
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserKey(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据令牌获取用户标识
     * 
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserKey(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据令牌获取用户ID
     * 
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserId(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.DETAILS_USER_ID);
    }

    /**
     * 根据身份信息获取用户ID
     * 
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserId(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USER_ID);
    }

    /**
     * 根据令牌获取用户名
     * 
     * @param token 令牌
     * @return 用户名
     */
    public static String getUserName(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取用户名
     * 
     * @param claims 身份信息
     * @return 用户名
     */
    public static String getUserName(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取键值
     * 
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key)
    {
        return Convert.toStr(claims.get(key), "");
    }
}

重点总结:

  • 密钥作为静态变量,导入

有用auth0包下的jwt的,也是工厂模式,方法名格式是create和withxxx

验证是给框架或者拦截器

JWT标准里面定义的标准claim包括:

  • iss(Issuser):JWT的签发主体;
  • sub(Subject):JWT的所有者;
  • aud(Audience):JWT的接收对象;
  • exp(Expiration time):JWT的过期时间;
  • nbf(Not Before):JWT的生效开始时间;
  • iat(Issued at):JWT的签发时间;
  • jti(JWT ID):是JWT的唯一标识。

token过期的续期方案

单token方案

图片

  • 将 token 过期时间设置为15分钟;
  • 前端发起请求,后端验证 token 是否过期;如果过期,前端发起刷新token请求,后端为前端返回一个新的token;
  • 前端用新的token发起请求,请求成功;
  • 如果要实现每隔72小时,必须重新登录,后端需要记录每次用户的登录时间;用户每次请求时,检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败,跳转到登录页面。

另外后端还可以记录刷新token的次数,比如最多刷新50次,如果达到50次,则不再允许刷新,需要用户重新授权。

在若依中,似乎没见前端调用refresh接口,,后端要是小于过期时间了就刷新(redis重新设置);

token要是失效了,直接抛异常(redis里查不到token就是失效,不用比较时间);

但是刷新token的比较时间却是直接获取当前登录用户,然后在java内存比较,,比起去redis找会快一些么,,这个用户放不放redis似乎关系不大了,,

双token方案
  • 登录成功以后,后端返回 access_tokenrefresh_token,客户端缓存此两种token;
  • 使用 access_token 请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token 调用token刷新接口获取新的 access_token;
  • 后端接受刷新token的请求后,检查 refresh_token 是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token 返回给客户端。
  • 客户端携带新的 access_token 重新调用上面的资源接口。
  • 客户端退出登录或修改密码后,注销旧的token,使 access_tokenrefresh_token 失效,同时清空客户端的 access_tokenrefresh_toke

微信网页授权是通过OAuth2.0机制实现的,也使用了双token方案。

微信网页授权方案
  • 用户在第三方应用的网页上完成微信授权以后,第三方应用可以获得 code(授权码)。code的超时时间为10分钟,一个code只能成功换取一次access_token即失效。
  • 第三方应用通过code获取网页授权凭证access_token和刷新凭证 refresh_token。
  • access_token是调用授权关系接口的调用凭证,由于access_token有效期(2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新。
  • refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。

后端实现token过期还可以利用Redis来存储token,设置redis的键值对的过期时间。如果发现redis中不存在token的记录,说明token已经过期了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值