JWT(Json·Web·Token)

@[TOC](JWT(Json Web Token))

1.简介

JWT全称 Json·Web·Token,是一个开放标准(RFC·7519),它定义了一种紧凑的,自包含的方式,用于作为JSON对象在各方之间安全的传输信息。该信息可以被验证和信任,因为它是数字签名的.

JWT是目前最流行的跨域身份解决方案

2.使用场景

下列场景中使用JWT是很有用的:

2.1 Authorization(授权)

这是使用JWT的常见场景。一旦用户登录,后续每个请求都将包含JWT允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松的跨域使用。

跨域:
比如你的网站有几个不同的后台的域名,你在a域名下登录,登录信息记录到cookie,那么后面访问a域名才会带cookie,其他域名是不会带cookie的。

浏览器的同源策略:
同源:如果两个 URL 的 protocol、port (如果有指定的话)和 host相同的话,则这两个URL 是同源

举例:
假如现在有一个url:http://store.company.com/dir/page.html

URL结果原因
http://store.company.com/dir2/other.html同源只有路径
http://store.company.com/dir/inner/another.html同源只有路径
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端⼝不同
http://news.company.com/dir/other.html失败主机不同

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档减少可能被攻击的媒介

因为有了浏览器同源策略的限制,所以有了跨域问题。

单点登录( SSO):Single Sign On:比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,⽤户只需要登录一次就可以访问所有相互信任的应用系统。

2.2 Information Exchange(信息交换)

对于安全的在各方之间传输信息⽽言,Json·Web·Token无疑是一种很好的方式。因为JWT可以被签名,例如,⽤公钥/私钥配对,你可以确定发送人就是他们所说的那个。另外,由于签名是使用头和有效负载计算的,您还可以验证内容有没有被篡改。

3.如何解决单点登录问题(集群为例)

在这里插入图片描述

3.1 方案1:Nginx负载均衡做iphash,同一个用户的请求永远只给到同一个后台服务器。

不足之处:(1)⼀个服务有若干个用户,有的用户活跃,向后端发起请求的次数比较频繁,(2)而有的用户不活跃,那么基于这种iphash的解决方案的话就势必会造成负载不均衡,(3)也就是说有的tomcat服务服务器压⼒大,而有的tomcat服务器压⼒不大,造成资源分配不均的情况。(4)其次就是假如有一个tomcat服务挂了,那么对于有一些用户来说,这个系统就挂了,失去了集群的意义。

3.2 方案2:基于tomcat广播的session复制

不足之处:每⼀个tomcat都需要维护⼀个大的session,会造成内存资源紧张

在这里插入图片描述

解决⽅案:使用JWT能够完美的解决上述问题

4.Token VS Session

4.1 基于session的身份认证

  • HTTP协议是无状态的,也就是说,如果已经认证了一个用户,那么下一次请求的时候,服务器不知道我是谁,须再次认证。
  • 传统的做法是将已经认证过的用户信息存储到服务器上,比如session。用户下次请求的时候带着sessionId,然后服务器检查用户是否已经认证过。

这种基于服务器的身份认证方式存在一些问题:

  1. Sessions:每次用户认证通过以后,服务器需要创建一条记录来保存用户信息,通常是在内存中。那么随着认证通过的用户越来越多,服务器在这里的开销就会越来越大。
  2. Scalability:由于session是在内存中的,这就带来一些扩展性的问题
  3. CORS:当我们想要扩展我们的应用,当我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题当使用AJAX调用另一个域名下获取资源时,我们可能会遇到禁止请求的问题
  4. CSRF:用户很容易受到CSRF的攻击

CSRF:跨站请求伪造(早期的攻击方式)

  1. 假设一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后者刚发布了一个具有Bob银行链接的图片信息
  2. 正常情况下,Bob点击图片链接访问银行,银⾏会让Bob登录
  3. 假如Bob在此之前刚好登录过了银行,浏览器中还有cookie,那么此时银⾏可能认为点击这个图片访问银⾏是Bob发出的,所以会正常给出响应。
  4. 假如这个链接是一个Alice伪造的转账的请求,那么Bob可能就会收到经济损失

4.2 基于Token的身份认证

基于Token的身份认证,在服务端不需要存储用户的登录信息,大概流程如下:

  1. 客户端使⽤用户名、密码请求登录
  2. 服务器收到请求去验证用户名和密码
  3. 验证成功之后服务端会签发一个token,再把这个token发送给客户端
  4. 客户端收到token以后可以把它存储起来,存到客户端内存或者其他地⽅
  5. 客户端每次向服务器请求资源的时候需要带着服务器签发的token
  6. 服务端收到请求,然后去验证客户端请求⾥面带着的token,如果验证成功,就向客户端返回请求的数据

4.3 对比

  1. 两者都可以存储用户信息,然而,session是把用户信息保存在服务端的,而JWT是把用户信息保存在客户端的,当然,也可以保存到服务端,甚至保存到数据库中。
  2. Session方式存储用户信息最大的问题在于要暂用服务器⼤量的内存,增加服务器器的开销。而基于token的方式将用户状态分散到了各个客户端中,可以明显的减轻服务端的内存压力。
  3. session的状态存储在服务端,客户端只有sessionId,而token的状态是存储在客户端
    在这里插入图片描述

4.4 使用Token的好处:

  • ⽆状态和可拓展性:Token 存储在客户端,完全无状态,可拓展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
  • 安全:Token不是cookie。每次请求的时候token都会被发送,可以作为请求参数发送,可以放在请求头里面发送,也可以放在cookie里面被发送。即使在你的实现中将token存储到客户端的cookie中,这个cookie也只是一种存储机制,而⾮身份认证机制。没有基于会话的信息可以操作,因为我们没有会话。

5.JWT token的基本格式

5.1 Header,Payload,Signature

JSON·Web·Token由三部分组成,他们之间用圆点(·)连接,这三部分分别是:

  • Header
  • Payload
  • Signature

5.2 Header由两部分信息组成:

  • type:声明类型,这里是jwt
  • alg:声明加密的算法 通常直接使用 HMAC SHA256

5.3 Payload就是存放有效信息的地方(不强制)

  1. iss: jwt签发者
  2. sub: jwt所面向的用户
  3. aud: 接收jwt的⼀方
  4. exp: jwt的过期时间,这个过期时间必须要大于签发时间
  5. nbf: 定义在什么时间之前,该jwt都是不可用的
  6. iat: jwt的签发时间
  7. jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
  8. claim:jwt存放信息的地方

5.4 Signature就是签名信息

因此,一个典型的JWT看起来是这个样子的:

xxxxxxxx·yyyyyyyyyy·zzzzzzzzzzz
具体如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3bGd6cyIsImV4cCI6MTU4Nzk3MzY1N
ywidXNlciI6Ijk2MkYxODkwNTVFMzRFNzVERjVGMzQ0QTgxODNCODdGIn0.APehq9dxRiilgTOGyuz
9qtZxvPDIJ5QIIVUCLYeX1QE

6.项目中如何使用

  1. 项目已经整合好了JWT,并且给我们提供了现有的工具类进⾏操作:
com.mall.user.utils.JwtTokenUtils
  1. 如何使⽤现有的工具创建JWT?
String token = JwtTokenUtils.builder().msg("xxx").build().creatJwtToken();
  1. 如何解析JWT呢?
String msg = JwtTokenUtils.builder().token(token).build().freeJwt();

7.所有代码

在这里插入图片描述
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>basic_skillTest</artifactId>
        <groupId>com.gy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gy</groupId>
    <artifactId>jwt</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.1</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.3</version>
        </dependency>
    </dependencies>

</project>

AESUtil

/**
 * @Author:gaoyuan
 * @Description:
 * @DateTime:2021/4/20 14:24
 **/
@Slf4j
public class AESUtil {
    //加密或解密内容
    @Setter
    private String content;
    //加密密钥
    private String secret;

    public AESUtil(String content) {
        this.content = content;
        this.secret = "iwofnoadnsa922342mnjaolkdsao9423242niosadopa_a02402sad";
    }

    /**
     * 加密
     * @return 加密后内容
     */
    public String encrypt () {
        Key key = getKey();
        byte[] result = null;
        try{
            //创建密码器
            Cipher cipher = Cipher.getInstance("AES");
            //初始化为加密模式
            cipher.init(Cipher.ENCRYPT_MODE,key);
            //加密
            result = cipher.doFinal(content.getBytes("UTF-8"));
        } catch (Exception e) {
            log.info("aes加密出错:"+e);
        }
        //将二进制转换成16进制
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < result.length; i++) {
            String hex = Integer.toHexString(result[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return  sb.toString();
    }

    /**
     * 解密
     * @return 解密后内容
     */
    public String decrypt () {
        //将16进制转为二进制
        if (content.length() < 1)
            return null;
        byte[] result = new byte[content.length()/2];
        for (int i = 0;i< content.length()/2; i++) {
            int high = Integer.parseInt(content.substring(i*2, i*2+1), 16);
            int low = Integer.parseInt(content.substring(i*2+1, i*2+2), 16);
            result[i] = (byte) (high * 16 + low);
        }

        Key key = getKey();
        byte[] decrypt = null;
        try{
            //创建密码器
            Cipher cipher = Cipher.getInstance("AES");
            //初始化为解密模式
            cipher.init(Cipher.DECRYPT_MODE,key);
            //解密
            decrypt = cipher.doFinal(result);
        } catch (Exception e) {
            log.info("aes解密出错:"+e);
        }
        assert decrypt != null;
        return new String(decrypt);
    }

    /**
     * 根据私钥内容获得私钥
     */
    private Key getKey () {
        SecretKey key = null;
        try {
            //创建密钥生成器
            KeyGenerator generator = KeyGenerator.getInstance("AES");
            //初始化密钥
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(secret.getBytes());
            generator.init(128,random);
            //生成密钥
            key = generator.generateKey();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return key;
    }

    public static void main(String[] args) {
        AESUtil aesUtil=new AESUtil("Hello");
        String ec=aesUtil.encrypt();
        System.out.println(ec);
        System.out.println(new AESUtil(ec).decrypt());
    }
}

JwtTokenUtils

@Slf4j
@Builder
public class JwtTokenUtils {
    /**
     * 传输信息,必须是json格式
     */
    private String msg;
    /**
     * 所验证的jwt
     */
    @Setter
    private String token;

    private final String secret="324iu23094u598ndsofhsiufhaf_+0wq-42q421jiosadiusadiasd";

    public String creatJwtToken () {
        msg = new AESUtil(msg).encrypt();//先对信息进行aes加密(防止被破解) AES 对称加密
        String token = null;
        try {
            token = JWT.create()
                    .withIssuer("zs").withExpiresAt(DateTime.now().plusDays(1).toDate())
                    .withClaim("user", msg)
                    .sign(Algorithm.HMAC256(secret));
        } catch (Exception e) {
            throw e;
        }
        log.info("加密后:" + token);
        return token;
    }

    public static void main(String[] args) {

//        String msg = "你好,JWT";
//        JwtTokenUtils tokenCreateor = JwtTokenUtils.builder().msg(msg).build();
//        String token = tokenCreateor.creatJwtToken();
//        System.out.println(token);

        String token = "pyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6cyIsImV4cCI6MTYxODkxMDc5MiwidXNlciI6IjgxMzQ4ODBFMUUyNjVBODE1MDc2MDc0NEQ2MDQyNkUyIn0.JlwuzwOnvqwTPpxCFDx7sMv6f-q0sYoimt4U4XRUAyA";
        String s = JwtTokenUtils.builder().token(token).build().freeJwt();
        System.out.println(s);


    }


    /**
     * 解密jwt并验证是否正确
     */
    public String freeJwt () {
        DecodedJWT decodedJWT = null;
        try {
            //使用hmac256加密算法
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
                    .withIssuer("zs")
                    .build();
            decodedJWT = verifier.verify(token);
            log.info("签名人:" + decodedJWT.getIssuer() + " 加密方式:" + decodedJWT.getAlgorithm() + " 携带信息:" + decodedJWT.getClaim("user").asString());
        } catch (Exception e) {
            log.info("jwt解密出现错误,jwt或私钥或签证人不正确");
            //throw new ValidateException(SysRetCodeConstants.TOKEN_VALID_FAILED.getCode()
            // ,SysRetCodeConstants.TOKEN_VALID_FAILED.getMessage());
            throw new  RuntimeException(" jwt 信息获取失败");
        }
        //获得token的头部,载荷和签名,只对比头部和载荷
        String [] headPayload = token.split("\\.");
        //获得jwt解密后头部
        String header = decodedJWT.getHeader();
        //获得jwt解密后载荷
        String payload = decodedJWT.getPayload();
        if(!header.equals(headPayload[0]) && !payload.equals(headPayload[1])){
            //throw new ValidateException(SysRetCodeConstants.TOKEN_VALID_FAILED.getCode(),SysRetCodeConstants.TOKEN_VALID_FAILED.getMessage());

            throw new  RuntimeException(" jwt 信息获取失败");
        }
        return new AESUtil(decodedJWT.getClaim("user").asString()).decrypt();
    }

}

结果:

创建jwt:
在这里插入图片描述

15:09:21.436 [main] INFO com.jwt.JwtTokenUtils - 加密后:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJpc3MiOiJ6cyIsImV4cCI6MTYxODk4ODk2MSwidXNlciI6IjgxMzQ4ODBFMUUyNjVBODE1MDc2MDc0NEQ2MDQyNkUyIn0
.6x-Z973YmWy_ostDoGhiMV0JNoX8Jzs6Iu6jcfkSxfs

解密;
在这里插入图片描述
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值