JSON Web Token

2019-07-23  本文已影响0人  Jerry_Liang

相信大家或多或少都接触过这个场景,那就是我们注册某个一个网站长账号后(账号A),网站会给我们的注册邮箱发送一封邮件,其中包含了激活链接,一般情况下,这个链接会在几个小时内过期,过期后就无法激活了。同样,你并不能使用这个激活链接来激活你又新注册的一个账号(此处称为账号B)。

JWT允许我们做到与上面场景同样的效果。在开始如何使用JWT实现类似的无状态验证前,先介绍下JWT的组成。

1.JWT的组成

一个JWT实际上就是一个字符串,它由三部分组成,分别是:头部载荷签名

头部(Header)

JWT的头部主要用于描述关于该JWT的最基本的信息,例如其类型以及签名所用算法等。这也可以表示成一个JSON对象。

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

在这里,头部说明了这是一个JWT,并且所用的签名算法为HS256。
对JSON对象进行Base64编码后,得到的字符串作为JWT的Header。示例字符串如下

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(Payload)

载荷就是存放有效信息的地方,它包含声明(要求)。声明有三种类型:

registered claims:标准中注册的声明。这里有一组预定义的声明,它们不是强制的,但是推荐
public claims:公共的声明
private claims:私有的声明

标准中注册的声明 (建议但不强制使用) :

{ 
  "iss": "Jerry", 
  "iat": 1563846005, 
  "exp": 1563856805, 
  "aud": "www.example.com", 
  "sub": "Jerry@example.com"
}

对上面的JSON对象进行Base64编码可以得到一串字符串。这个字符串我们将它称作JWT的Payload。示例:

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

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

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
对Payload进行Base64加密就得到了JWT第二部分的内容。

将头部与载荷两个编码后的字符串用句号.连接在一起(头部在前),就形成了JWT的一部分,如下

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
签名(Signature)

最后,将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加上我们的签名后,使用HS256算法进行加密结果示例如下:

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

加密后得到的字符串就是我们的签名,我们将这部分与前面的字符串拼接,就得到了完整的JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

参考图:


image.png
签名目的

最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。

所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。

如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

2. 回顾文章开头的场景

在了解了JWT的生成过程后,你猜到了开头场景是如何实现的了嘛?
没错,其中:

3. JWT使用场景

4. Token对比Session验证机制

4.1 基于session的验证机制

http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来:

针对扩展性,如果我们要实现分布式部署,需要我们做好集群间session的共享,这样我们才能很好的扩展我们的应用。

4.2 基于Token的验证机制

Token机制相对于传统的session认证(即Cookie机制),有以下的好处:

我们也知道,很难有一种技术是万能的,无懈可击的,基于Token的验证机制同样有它的缺点:

针对注销,我们可以清空或者修改服务端的用户对应的secret(这种情况下,我们的每个用户secret都是不一样的,即和用户关联,而不是使用统一的secret),这样用户在注销后,JWT本身不变,但是由于secret不存在或者改变,则无法完成校验。(仅做参考,我们可以按照我们实际需求设计)

针对于续签问题,我们可以考虑这一种方案:
服务端JWT过期时间依旧按常规设置,签发时同时存入redis,在redis中的过期时间为服务端签发的两倍(依个人而设置,大于服务端存储的就好)。验证的时候正常进行:

续签可以很好的解决这样的问题:那就是如果用户非常活跃,用户在使用超过我们的过期时间还在使用系统,那么他又得重新登录。用户体验性会很差,所以需要我们增加续签的功能来提高用户体验。

5. Springboot中的JWT实现

5.1 引入依赖

pom.xml中加入我们的依赖

 <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Uncomment this next dependency if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:  -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.60</version>
            <scope>runtime</scope>
        </dependency>

编写我们的TokenUtil.class 以下为示例代码:

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;

import io.jsonwebtoken.*;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.slf4j.*;


/**
 * @Author: JerryLiang
 * @Date: 2019/7/15 10:53
 **/
public class TokenUtil {

    private final static Logger logger = LoggerFactory.getLogger(TokenUtil.class);

    private final static String myApiKeySecret = "这里写入你的Secret";


    /**
     * 创建JSON WEB TOKEN
     * @param id
     * @param userName
     * @param userPower
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String userName, String userPower, long ttlMillis){

        //设置签名算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //设置密钥
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(myApiKeySecret);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //设置JWT claims
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                .setId(id)
                .setIssuedAt(now)
                .setAudience("iot")
                .setIssuer("Jerry")  //设置发行者,自定义
                .claim("userName", userName)
                .claim("userPower", userPower)
                .signWith(signatureAlgorithm, signingKey);

        //设置超时时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        //生成JWT
        return builder.compact();

    }

    /**
     * 解析JWT,并验证用户权限
     * @param jwt
     */
    public static Boolean parseJWT(String jwt) throws ParseException {

        if (jwt == null) {
            logger.error("----------Token不能为空------------");
            return false;
        }
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(myApiKeySecret))
                    .parseClaimsJws(jwt).getBody();

            //将超时时间格式化为时间戳time
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
            String timeFormat = sdf.format(claims.getExpiration());

            Date date = sdf.parse(timeFormat);
            long time = date.getTime();
            long currentTime = System.currentTimeMillis();


            return ("Jerry").equals(claims.getIssuer()) &&
                    ("iot").equals(claims.getAudience()) &&
                    (time > currentTime) &&
                    claims.get("userName") != null;

        }catch (Exception e){
            e.printStackTrace();
            logger.error("----------Token格式有误------------");
            return false;
        }
    }

    /**
     * 获取jwt中的userName
     * @param jwt
     * @return
     */
   public static String getUserName(String jwt){

       Claims claims = Jwts.parser()
               .setSigningKey(DatatypeConverter.parseBase64Binary(myApiKeySecret))
               .parseClaimsJws(jwt).getBody();

       return claims.get("userName").toString();
   }

}

我们在需要验证的接口中调用TokenUtil中的方法就可以了。示例如下

if(TokenUtil.parseJWT(jwt)){
      //此处为验证通过执行的代码
}else{
      //此处为验证不通过执行的代码
}

至此,基于JWT的验证已经完成了,但是上面的代码中我并没有加入解决续签问题的解决方案的示例代码,可以参考我的另一篇关于springboot整合redis的文章,我们在createJWT函数中,生成完后将JWT存入redis即可,同时,在使用parseJWT解析认证后,若失败,则查询redis是否存在该JWT,若存在则重新调用createJWT签发新的JWT,再存入redis中即可。

注意:大家或许可能会想,这里借助redis存储JWT,是不是性质已经和session差不多了呀?其实不然,此处存储的目的是解决续签问题,而并没有实际的验证比对,所以并不是一样的哈!

参考

http://blog.didispace.com/learn-how-to-use-jwt-xjf/

https://www.cnblogs.com/hongdiandian/p/9294970.html

https://blog.csdn.net/qq_38306688/article/details/88643545

上一篇下一篇

猜你喜欢

热点阅读