3、JWT深入理解及实战

JWT深入理解及实战

1、概念

JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可 以解析检验token。

2、三要素

  • 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
  • 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
  • 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

3、安全性分析

从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其

中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐

上面了!

试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术

公开了?大家可以用这个盐来解析token,就能用来伪造token。

这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全

效果!

4、组成详解

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

4.1、头部(Header)
头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也 

可以被表示成一个 JSON 对象。 

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

105106 

在 头 部 指 明 了 签 名 算 法 是 HS256 算 法 。 

我 们 进 行 BASE64 编 码 

( http://base64.xpcha.com/ ) , 编 码 后 的 字 符 串 如 下 : 

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 
4.2、载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包

含三个部分:

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

iss: jwt 签发者

sub: jwt 所面向的用户

aud: 接收 jwt 的一方

exp: jwt 的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该 jwt 都是不可用的.

iat: jwt 的签发时间

jti: jwt 的唯一身份标识,主要用来作为一次性 token

2 .公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.

但不建议添加敏感信息,因为该部分在客户端可解密。

3. 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为

base64 是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的 claim。比如前面那个结构举例中的 admin 和 name 都属

于自定的 claim。这些 claim 跟 JWT 标准规定的 claim 区别在于:JWT 规定的 claim,

JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不

知道是否能够验证);而 private claims 不会验证,除非明确告诉接收方要对这些 claim

进行验证以及规则才行。定义一个 payload:

{“sub”:“1234567890”,“name”:“John Doe”,“admin”:true}

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRta

W4iOnRydWV9

4.3、签证(signature)

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

header (base64 后的)

payload (base64 后的)

secret

这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.连接组成

的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了

jwt 的第三部分。107

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFt

ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjo

YZgeFONFh7HgQ

注意

secret 是保存在服务器端的,

jwt 的签发生成也是在服务器端的,secret 就是用来进行

jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。

一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt 了。

5、使用场景

1. 一次性验证

比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接

需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),

不能被篡改以激活其它可能的账户……这种场景就和 jwt 的特性非常贴近,jwt 的

payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

2.restful api 的无状态认证

使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务

端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改…spring

security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2

或 jwt 来做 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后

者实现简单。

3.使用 jwt 做单点登录+会话管理(不推荐)

6、 工具类

导包

"io.jsonwebtoken:jjwt-api:$jsonwebtoken_version"
"io.jsonwebtoken:jjwt-impl:$jsonwebtoken_version"
"io.jsonwebtoken:jjwt-jackson:$jsonwebtoken_version"

工具一共如下Payload、JwtUtils、JwtClaim三个:

Payload

import lombok.Data;
import java.util.Date;

/**
 * @author :
 * @date :Created in 19:30 2022/7/18
 * @description :
 * @version: 1.0
 */
@Data
public class Payload<T> {
    private String id;
    private T userInfo;
    private Date expiration;
}

JwtClaim

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author :
 * @date :Created in 19:27 2022/7/18
 * @description :Payload泛型 
 * @version: 1.0
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JwtClaim {

    /**
     *登录账号名,唯一
     */
    private String username;


    /**
     * 业务入口
     */
    private Integer clientType;

}

JwtUtils

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.UUID;

/**
 * @author :
 * @date :Created in 16:30 2022/7/18
 * @description :
 * @version: 1.0
 */
public class JwtUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "userinfo";

    public static final String BEARER_PREFIX = "Bearer ";

    public static final int EXPIRE_TIME = 24 * 60 * 3;

    /**
     * 私钥加密token
     *
     * @param jwtClaim   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位分钟
     * @return JWT
     */
    public static String generateTokenExpireInMinutes(Object jwtClaim, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, GsonSerializeUtils.toString(jwtClaim))
                .setId(createJTI())
                .setExpiration(DateTime.now().plusMinutes(expire).toDate())
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位秒
     * @return JWT
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, GsonSerializeUtils.toString(userInfo))
                .setId(createJTI())
                .setExpiration(DateTime.now().plusSeconds(expire).toDate())
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return Jws<Claims>
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(GsonSerializeUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白de成长之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值