JWT(JSON WEB TOKEN)

Updated on with 0 views and 0 comments

http协议是无状态协议,用户与后端服务的交互都是”无状态的“。互联网服务是永远离不开用户认证。一般流程是下面这样。

  1. 用户向服务器发送用户名和密码。

  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。

  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性不好。单机当然没有任何问题,但是如果是在集群环境下,或者是跨域的架构,就要考虑到session共享的问题。
现在项目开发基本都是前后分离,尤其是针对些App开发,App可没有session,cookies这些概念。

后来我们摒弃了上面的做法,我们引入的token机制,token是一个概念模型,就是为了方便用户不用每次访问页面都输入用户名密码来做验证。现在流程是这样的

  • 用户使用用户名密码请求服务器

  • 服务器进行验证用户信息

  • 服务器通过验证返回给用户一个token,自身也会留存一份

  • 客户端存储token,并在每次请求时附加这个token值

  • 服务器验证token,并返回数据

这个token必须要在每次请求时发送给服务器,它应该保存在请求头中,另外,服务器要支持CORS(跨域资源共享机制)策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*(允许所有域名访问)

我做过不少个项目的用户认证这一块都是使用上面的这种方式。
这里token是需要”持久化“的,内存,关系型数据库,非关系数据库(都放过。。)
token放入内存,后期集群不允许。
token放入关系型数据库,每次需要验证请求合法性都要额外与书数据库建立连接。
于是便放在非关系型数据库中,但是当用户量较大时,上面无论哪一种都会有产生问题。
再说下上面这种方式的SSO问题,有两个系统分别是:liqitian.comliqitian.top,我们要实现他们的SSO,基本上我们都需要单独在提出一个专门认证服务,如sso.com,后面所有的登录和身份验证都在sso.com中操作。
见下图
image.png

JWT

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,后面,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

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

JWT是由三部分构成,将这三段信息文本用链接构成了JWT字符串。就像这样
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U
格式为:A.B.C

JWT 是由三个部分依次组成的如下

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

下面依次介绍这三个部分。

Header

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

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

alg(algorithm)属性表示签名的算法,默认是 HMAC SHA256(写成 HS256);typ(type)属性表示这个令牌(token)的类型,JWT 令牌统一写为JWT
最后,将上面的 JSON 对象使用 Base64编码转成字符串构成了第一部分,如下

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):接收jwt的一方
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。


{
  "sub": "123123",
  "name": "liqitian3344",
  "admin": true
}

然后将其base64编码,得到jwt的第二部分如下

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

Payload可以添加任何的信息,一般添加用户的相关信息或其它业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密

Signature

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

我们需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

base64加密后的header和base64加密后的payload使用“.”连接组成的字符串,然后通过header中声明的加密方式进行加secret组合加密,就构成了jwt的第三部分

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

我们将这三部分用“.”连接成一个完整的字符串,构成了最终的jwt。
前后端交互流程如下:
image.png

JWT优缺点

  • 因为json的通用性,所以JWT是可以跨语言支持的,像C#,JavaScript,NodeJS,PHP等许多语言都可以使用
  • 因为由了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息
  • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的
  • 它不需要在服务端保存会话信息,所以它易于应用的扩展
  • 不应该在jwt的payload部分存储敏感信息,因为该部分是客户端可解密的部分
  • 保护好secret私钥。该私钥非常重要
  • 如果可以,请使用https协议

只能被动等到token过期,不能主动失效token(这点挺致命的,但是在服务端设置加密的 secret,为每个用户生成唯一的 secret,失效则改变该 secret便可解决,但是增加了一次query的过程。)

下面看下精简版的代码

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

------------------------------------------------------------------------------------------

jwt:
 # 发行者
 name: liqitian3344
 # 密钥, 经过Base64加密, 可自行替换
 base64Secret: SGlsb3g=
 #jwt中过期时间设置(分)
 jwtExpires: 1
------------------------------------------------------------------------------------------
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtParam {

    /**
     * 发行者名
     */
    private String name;

    /**
     * base64加密密钥
     */
    private String base64Secret;

    /**
     * jwt中过期时间设置(分)
     */
    private int jwtExpires;
}
------------------------------------------------------------------------------------------

 /**
     * 生成token, 用户登录成功可调用
     *
     * @param userId   用户id
     * @param claim    可选参数
     * @param jwtParam JWT官方字段
     * @return
     */
    public static String createToken(String userId, Map<String, Object> claim, JwtParam jwtParam) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            // 生成签名密钥
            byte[] apiKeySecretBytes =
                    DatatypeConverter.parseBase64Binary(jwtParam.getBase64Secret());
            SecretKeySpec signingKey =
                    new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            // 添加构成JWT的参数
            JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                    .claim("userId", userId)
                    .addClaims(claim)
                    .setIssuer(jwtParam.getName())
                    .setIssuedAt(now)
                    .signWith(signatureAlgorithm, signingKey);

            // 添加token过期时间
            long TTLMillis = jwtParam.getJwtExpires() * 60 * 1000;
            long expMillis = nowMillis + TTLMillis;
            Date exp = new Date(expMillis);
            jwtBuilder.setExpiration(exp).setNotBefore(now);
            return jwtBuilder.compact();
        } catch (Exception e) {
            log.error("签名失败", e);
            return null;
        }
    }
------------------------------------------------------------------------------------------

 /**
     * 验证解析token
     * @param authToken    授权token信息
     * @param base64Secret 加密密钥
     * @return
     */
    public static Claims parseToken(String authToken, String base64Secret) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Secret))
                    .parseClaimsJws(authToken).getBody();
            return claims;
        } catch (SignatureException se) {
            // TODO 这里自行抛出异常
            log.error("===== 密钥不匹配 =====", se);
        } catch (ExpiredJwtException ejw) {
            // TODO 这里自行抛出异常
            log.error("===== token过期 =====", ejw);
        } catch (Exception e) {
            // TODO 这里自行抛出异常
            log.error("===== token解析异常 =====", e);
        }
        return null;
    }

end ~~


标题:JWT(JSON WEB TOKEN)
作者:liqitian3344
地址:https://liqitian.com/articles/2019/11/04/1572834876894.html