SpringSecurity系列——JWT(jjwt)day1-2

181 篇文章 3 订阅
24 篇文章 31 订阅

简介

jwt(JSON Web Tokens)
JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。 此信息可以验证和信任,因为它是数字签名的。 JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

虽然 JWT 可以加密以在各方之间提供保密性,但我们将专注于签名令牌。 签名的令牌可以验证其中包含的声明的完整性,而加密的令牌会向其他方隐藏这些声明。 当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是签署它的一方。

官网地址

https://jwt.io/
在这里插入图片描述
从这张图我们可以看出来jjwt是在Java中支持最好的,所以我们直接学这个

Session对比JWT

session

http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

问题
  1. 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
  2. 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
  3. 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
  4. 前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。

JWT

在这里插入图片描述
在前后端分离的项目中我们使用jwt生成的token令牌进行校验工作
校验通过响应请求,校验不通过直接拒绝

JWT的优点
  1. 简洁小巧:可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  2. 自包含︰负载中包含了所有用户所需要的信息,避免了多次查询数据库
  3. 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
  4. 不需要在服务端保存会话信息,特别适用于分布式微服务。

JWT的结构

分为三个部分

  1. header(标头)
  2. payload(有效载荷)
  3. signature(签名)
header.payload.signature

Header

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HNAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

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

Payload

有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT 结构的第二部分

Signature

Signature 需要使用编码后的header和 payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

JJWT

地址

https://github.com/jwtk/jjwt

JJWT基础使用和介绍

导入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
quickstart
package com.example.ss1.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class JWTUtil {
    public static void main(String[] args) {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS384);
        String userName = Jwts.builder().setSubject("userName").signWith(secretKey).compact();

        System.out.println(userName);
    }
}

在这里插入图片描述

代码解释

在quickstart中
在这里插入图片描述

设置使用加密算法
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS384);

在这里插入图片描述

构建jwt
Jwts.builder()
设置payload中包含的用户信息
.setSubject("userName")
设置加密算法
.signWith(secretKey)
压缩生成token令牌
.compact()
错误一:ES系列算法不支持

jjwt不支持ES256,ES512,ES384所以如果你使用Keys.secretKeyFor(SignatureAlgorithm.ES256)就会看到如下报错信息

Exception in thread "main" java.lang.IllegalArgumentException: The ES256 algorithm does not support shared secret keys.
	at io.jsonwebtoken.security.Keys.secretKeyFor(Keys.java:140)
	at com.example.ss1.util.JWTUtil.main(JWTUtil.java:11)
解决

很简单将ES系类换成HS系列即可

算法

JWT HMAC-SHA 签名算法 HS256、HS384 和 HS512 要求密钥的位数至少与 RFC 7512 第 3.2 节中算法的签名(摘要)长度一样多。 这表示:

HS256 是 HMAC-SHA-256,它会产生 256 位(32 字节)长的摘要,因此 HS256 要求您使用至少 32 字节长的密钥。

HS384 是 HMAC-SHA-384,它会生成 384 位(48 字节)长的摘要,因此 HS384 要求您使用至少 48 字节长的密钥。

HS512 是 HMAC-SHA-512,它会生成 512 位(64 字节)长的摘要,因此 HS512 要求您使用至少 64 字节长的密钥。


JWT RSA 签名算法 RS256、RS384、RS512、PS256、PS384 和 PS512 都需要 2048 位的最小密钥长度(也称为 RSA 模数位长度)。 任何小于此值(例如 1024 位)的内容都将被拒绝并返回 InvalidKeyException。

也就是说,为了与最佳实践保持一致并增加密钥长度以延长安全寿命,JJWT 建议您使用:

RS256 和 PS256 至少 2048 位密钥

至少 3072 位密钥,带 RS384 和 PS384

RS512 和 PS512 至少 4096 位密钥

这些只是 JJWT 建议而不是要求。 JJWT 仅强制执行 JWT 规范要求,对于任何 RSA 密钥,要求是 RSA 密钥(模数)长度(以位为单位)必须 >= 2048 位。


生成JWT并解析

JWT解析时需要捕获JwtException异常

String subject = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(userName).getBody().getSubject();
解析构造
Jwts.parserBuilder()
设置解析算法
.setSigningKey(secretKey)
构造
.build()
解析目标JWT,获取JWT体,获取payload中的内容
.parseClaimsJws(userName).getBody().getSubject();
完整代码
package com.example.ss1.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class JWTUtil {
    public static void main(String[] args) {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS384);
        String userName = Jwts.builder().setSubject("userName").signWith(secretKey).compact();

        System.out.println(userName);
        //解析JWT
        String subject = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(userName).getBody().getSubject();
        System.out.println(subject);
    }
}

JJWT构造中的设置payload

1.放置一个(setSubject()方法)
setSubject("userName")
2.设置头(setHeaderParam()方法)

放置多个时使用的是Map来存

String jws = Jwts.builder()

    .setHeaderParam("kid", "myKeyId")
标准声明

在JwtBuilder提供了在JWT规范中定义的标准注册权利要求名方便setter方法。他们是:

setIssuer:设置iss(发行方)索赔
setSubject:设置sub(主题)声明
setAudience:设置aud(受众群体)声明
setExpiration:设置exp(到期时间)声明
setNotBefore:设置nbf(不早于)声明
setIssuedAt:设置iat(签发)声明
setId:设置jti(JWT ID)声明
3.设置标准声明
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.HOUR,10);
        Date expiration = instance.getTime();

        Jwts.builder()
                .setIssuer("发行方")
                .setSubject("主题声明")
                .setAudience("受众群体")
                .setExpiration(expiration) //a java.util.Date
                .setNotBefore(expiration) //a java.util.Date
                .setIssuedAt(expiration) // for example, now
                .setId(String.valueOf(UUID.randomUUID())); //just an example id
4.自定义声明

果您需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,则可以JwtBuilder claim根据需要简单地调用一次或多次:

        Claims claims = Jwts.claims();
        claims.put("userId",54564);
        claims.put("userName","zhangsan");
        Jwts.builder().setClaims(claims);

JJWT解析

  1. 使用该Jwts.parserBuilder()方法创建JwtParserBuilder实例。
  2. 指定要用于验证JWS签名的SecretKey或不对称PublicKey。1个
  3. 在build()上调用方法JwtParserBuilder以返回线程安全JwtParser。
  4. 最后,parseClaimsJws(String)用您的jws调用该方法String,生成原始的JWS。
  5. 如果解析或签名验证失败,则整个调用将包装在try / catch块中。稍后我们将讨论异常和失败原因。

注意:如果您期望使用JWS,请始终调用JwtParser的parseClaimsJws方法(而不是其他可用的类似方法之一),因为这可以保证解析签名的JWT的正确安全模型。

Jws<Claims> jws;

try {
    jws = Jwts.parserBuilder()  // (1)
    .setSigningKey(key)         // (2)
    .build()                    // (3)
    .parseClaimsJws(jwsString); // (4)
    
    // we can safely trust the JWT
     
catch (JwtException ex) {       // (5)
    
    // we *cannot* use the JWT as intended by its creator
}

设置Claim进行解析

package com.example.ss1.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

public class JWTUtil {
    public static void main(String[] args) {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS384);
        Claims claims = Jwts.claims();
        claims.put("userId",1234);
        claims.put("userName","zhangsan");
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(secretKey)
                .compact();

        System.out.println(token);
        //解析JWT
        Claims body = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build().parseClaimsJws(token).getBody();

        Object userId = body.get("userId");
        System.out.println(userId);
    }
}

在这里插入图片描述

JJWTUtil的封装(简单)

package com.example.ss1.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;

public class JJWTUtil {
    //默认密钥
    public final static String DEFAULT_SECRET_KEY = "self_define_secret_key_about_the_project";

    public SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(DEFAULT_SECRET_KEY.getBytes(StandardCharsets.UTF_8));
    }

    //设置Claim
    public Claims setClaims(Integer userId, String username) {
        Claims claims = Jwts.claims();
        claims.put("userId", userId);
        claims.put("username", username);
        return claims;
    }

    //构造token
    public String createToken(Claims claims) {
        //默认设置七天
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 60 * 60 * 24 * 7);
        String token = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(calendar.getTime())
                .signWith(getSecretKey(), SignatureAlgorithm.HS256)
                .compact();
        return token;
    }

    //解析token判断是否过期,过期为false,未过期为true
    public Boolean parseToken(String token) {
        boolean flag = false;
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSecretKey())
                    .build()
                    .parseClaimsJws(token);
            flag = true;
        } catch (JwtException e) {
            e.getMessage();
        }
        return flag;
    }

    //获取header
    public JwsHeader getHeader(String token) {
        Boolean exceptionJudge = parseToken(token);
        JwsHeader header = null;
        if (exceptionJudge) {
            header = Jwts.parserBuilder()
                    .setSigningKey(getSecretKey())
                    .build()
                    .parseClaimsJws(token)
                    .getHeader();
        }else {
            System.out.println("token已过期");
        }
        return header;
    }
    //获取payload
    public Claims getPayload(String token) {
        Boolean exceptionJudge = parseToken(token);
        Claims body = null;
        if (exceptionJudge) {
             body = Jwts.parserBuilder()
                    .setSigningKey(getSecretKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

        }else {
            System.out.println("token已过期");
        }
        return body;
    }
    //获取时间数据
    public Object getExceptionTime(String token) {
        Boolean exceptionJudge = parseToken(token);
        Claims body = null;
        if (exceptionJudge) {
            body = Jwts.parserBuilder()
                    .setSigningKey(getSecretKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }else {
            System.out.println("token已过期");
        }
        Object exp = body.get("exp");
        return exp;
    }
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值