原创

JWT令牌

前言

什么是JWT ?

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

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

头部(Header)

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

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

在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

载荷(playload)

载荷就是存放有效信息的地方。

定义一个payload:

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

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

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

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

5.3 JJWT签发与验证token

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

5.3.1 创建token

(1)新建项目jwtTest中的pom.xml中添加依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

(2) 创建测试类,代码如下

JwtBuilder builder= Jwts.builder()
    .setId("888")   //设置唯一编号
    .setSubject("小白")//设置主题  可以是JSON数据
    .setIssuedAt(new Date())//设置签发日期
    .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串 
System.out.println( builder.compact() );

运行打印结果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

5.3.2 解析token

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";

Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();

System.out.println(claims);

运行打印效果:

{jti=888, sub=小白, iat=1557904181}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

5.3.3 设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

(1)创建token 并设置过期时间

//当前时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
    .setId("888")   //设置唯一编号
    .setSubject("小白")//设置主题  可以是JSON数据
    .setIssuedAt(new Date())//设置签发日期
    .setExpiration(date)
    .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );

解释:

.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据

运行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI

(2)解析TOKEN

String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";

Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();

System.out.println(claims);

当前时间超过过期时间,则会报错。

5.3.4 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建测试类,并设置测试方法:

创建token:

@Test
public void createJWT(){
    //当前时间
    long currentTimeMillis = System.currentTimeMillis();
    currentTimeMillis+=1000000L;
    Date date = new Date(currentTimeMillis);
    JwtBuilder builder= Jwts.builder()
        .setId("888")   //设置唯一编号
        .setSubject("小白")//设置主题  可以是JSON数据
        .setIssuedAt(new Date())//设置签发日期
        .setExpiration(date)//设置过期时间
        .claim("roles","admin")//设置角色
        .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
    //构建 并返回一个字符串
    System.out.println( builder.compact() );
}

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI

解析TOKEN:

//解析
@Test
public void parseJWT(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI";
    Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
    System.out.println(claims);
}

6.springSecurity RSA非对称加密算法生成JWT

/**
     * 使用证书的私钥作为签名
     */
    @Test
    public void CreateToken(){
        //读取证书文件
        ClassPathResource resource = new ClassPathResource("changgou68.jks");
        //读取证书数据, 参数: 证书密码
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,"changgou68".toCharArray());
        //获取一对私钥和公钥 参数:秘钥别名,秘钥密码
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair("changgou68","changgou68".toCharArray());
        PublicKey publicKey = keyPair.getPublic();//公钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();//私钥

        //载荷
        Map<String,Object> payload = new HashMap<>();
        payload.put("nikename","tom");
        payload.put("age","19");
        payload.put("role","baby");
        //生成JWT
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(payload), new RsaSigner(privateKey));
        //获取令牌
        String token = jwt.getEncoded();
        System.out.println(token);
    }


    /**
     * 解析 非对称加密后的JWT
     */
    @Test
    public void parseToken(){
        String token="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYmFieSIsIm5pa2VuYW1lIjoi5L-e6I6J5ZCbIiwiYWdlIjoiMTkifQ.RUnP5gWh1OeDOAMXW9gUpNHVd_ZJ7S8GsULmbhJxNj_cCJTtncJP_4pmNFZuDiwevh0C102uJXnWoOA0V0Cfu90qZH8KgxXfO4L4XR044wkomFGJ82g23Kp-PibwaT8IMC2mhxvYp1_FMO21Z-puiAuEFrsbd45FksTu9mHjjUQJv6r9BeelqWz700PVroccItDVIMBJJrU_eh5uSRa48vZJaAnUnNpslPNpnuJpHtn4k9dTNCRmf8a_OpWRiy4lynk7WHMrV2cuWswVJhHXD2ly5tlDy4CJrv7rt5Gzgbhv0kaasuMk35dgACV_wvP2JTFL3RyINvwFSLit6tFRpg";
        String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhxkqdUBeQRi1ek5bo/blu8DjQ5aqqQKye/VqW7vS2Fz+v2NMti4XFzhFIxcHLE7q9emJJIMneV497MiAIkK8dqAqx/o5thZmmchpuDCxPY8PTX2CE175hml0aP0r6LyyzrkzNMcggQRrtgsR3oyeDVpkJbgZOj3knpNnnk7KGQz//Cg20YepSz0MltssUSwOdTLkT/0fFXmRbAf/9bjc1HP2Szt69J87Cn/itHO3UIe6fYmjzKZ+CrUEbehHkqzN12iesb/swW3EpG6037QeR2hAIOUXpbE6RmYpAsBlR5yVwLiHzmkxCy7LUWkpQBbe9NcRoqTjxgPE/r0RJi1TBQIDAQAB-----END PUBLIC KEY-----";

        //使用公钥 解析加密后的token
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));

        //获取载荷数据
        String claims = jwt.getClaims();
        System.out.println(claims);
    }
<build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.jks</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*.jks</include>
                </includes>
            </resource>
        </resources>
    </build>
Java

留言板