Java - JWT的简单介绍和使用

Posted Zong_0915

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java - JWT的简单介绍和使用相关的知识,希望对你有一定的参考价值。

Java - JWT的简单介绍和使用

前言

目前自己在做一个云直播个人项目,后端架构是微服务,目前准备用JWT来做Token的校验。借此机会来复习和学习一遍JWT的相关知识。

一. JWT 基础知识

JWT的全称是JSON Web Tokens主要是服务器认证相关信息之后,生成一种JSON数据,并通过一种算法(例如HMAC)对其进行安全的签名加密。主要用于两个用途:

  • 数据交换:因为JWT使用JSON来存储相关数据,而JSON这一种数据格式可以在各方之间传递。
  • 安全验证:登录之后,后续的请求可以携带JWT,只有携带了JWT(认证Token)的请求才能够正常地访问到相关的数据及资源。

一般我们在开发过程中,凡是涉及到用户登录的,我们就需要去考虑用什么去存储用户登录的一个状态,本文只说两种:

  • session(基于cookie的实现):只不过session相关的数据存储于服务器。
  • JWT:一旦生成,一般就抛给客户端去保存。客户端只需要每次携带这个Token就可以正常地访问接口。

1.1 session 案例测试

我们来写一个简单的案例:pom依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Controller代码:

@RestController
public class UserController 
    @GetMapping("/login")
    public String login(HttpServletRequest request) 
        request.getSession().setAttribute("user", "Ljj");
        return "Login Success";
    

	@GetMapping("/getUser")
    public String getUser(HttpServletRequest request) 
        String user = (String) request.getSession().getAttribute("user");
        return user;
    

那么我们项目启动之后,访问以下路径:http://localhost:8080/login,就能发现当前客户端会多出一个名为JSESSIONIDCookie

之后我们的每一次请求,都会自动携带上这个cookie去访问服务端:

如果我们采取session来存储用户信息,那么大致流程就是如下:

  1. 用户登录成功,那么我们就request.getSession().setAttribute("user"+userId, "用户信息")
  2. 此时服务端就会写入一个session来保存相关的数据(基于cookie实现)。因此客户端能看到一个名为JSESSIONIDCookie
  3. 后续的相关请求,都会自动携带这个Cookie,后端就可以取到这个用户信息了。

但是,采用session这种方式,是以本地缓存来存储相关的数据的,当有100个用户进行登录的时候,就需要在本地缓存存储100个信息。因此会给服务器带来一定的内存压力。

相关的数据存储于StandardSession.attributes字段中:

public class StandardSession implements HttpSession, Session, Serializable 
	protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();

因此现在主流的都是使用JWT来代替传统的session存储方案。

1.2 JWT 结构

JWT如上文所说,他是一个JSON串,但是它有着自己的特定结构:

Header.Payload.Signature

HeaderPayload部分都是一个base64编码串,三个结构之间通过 ” . “ 进行连接。

1.2.1 Header

Header头部,通常有两个部分组成:

  • alg:表示签名的算法类型,比如HMAC SHA256 或者 RSA
  • typ:代表这个token令牌的类型,比如JWT

案例如下:


  "alg": "HS256",
  "typ": "JWT"

最后这个JSON串会通过Base64Url进行编码,然后作为JWT的第一部分。

1.2.2 Payload

JWT的第二部分就是这个Payload了,它的中文含义叫做:有效载荷。相当于我们HTTP请求的一个请求体了。一般用来存储我们实际要传递的数据。例如:


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

Payload本身还规定了几个属性,同时它可以分为三种类型:

标准注册声明:

  • iss (issuer):签发人。
  • exp (expiration time):过期时间。
  • sub (subject):主题。
  • aud (audience):接收方。
  • nbf (not before):生效时间。
  • iat (issued at):签发时间。
  • jti (jwt id):编号。

公共声明: 一般用于我们自己定义一些业务属性。


私有声明: 服务器和客户端共同定义的声明

这一部分的内容可见比较丰富,因为我们可以将自定义的属性塞进去,但是有一点我们需要格外地注意:Payload部分不建议添加敏感信息,例如密码、身份证等信息。因为Payload这一部分最终也是经过Base64Url进行编码,然后作为JWT的第二部分暴露给客户端。因此这个是可以被解码的。

1.2.3 Signature ☆

上面的两个JWT组成部分:HeaderPayload,我们知道它是由Base64Url进行编码的,那么自然而然的它就可以被解码。那么JWT的安全性谈何而来?这就得看最后一个部分Signature签名了。他的作用一句话概括就是:对前两部分的内容进行算法签名,防止数据篡改。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

这个代码是啥意思呢?

  1. 拿到经过Base64编码的HeaderPayload部分。以及我们自己提供的一个秘钥secret
  2. 使用Header中指定的签名算法:HMAC SHA256 进行签名。

签名的目的:

  1. 如果有人对头部以负载内容进行解码,然后篡改相关信息,在进行base64编码重新组合一个新的JWT
  2. 那么这个JWT传输给服务器后,服务器能够判断,新的头部和新的负载内容形成的签名和原有的不一致。
  3. 那么此时就能够判断这个JWT是不合法的。因为外部使用者无法得知你进行签名时的秘钥是什么。 因此通过修改JWT算出来签名是不一样的。

那么最后再来说下JWT的特点:

  • JWT的两个组成部分虽然是经过base64编码的,因此他可以被解码。但是JWT本身是可以被加密的。这样就更加安全。
  • JWT生成之后,不必存储在服务器端,就没有使用session存储那样,有着内存压力。
  • JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改。
  • 安全:因为Token不是Cookie。我们只需要每次请求的时候携带Token即可。由于没有Cookie被发送,还有助于防止CSRF攻击。
  • 不要再JWT中存储一些敏感信息,一般都是存储userId

二. JWT 简单使用

首先准备pom依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.1</version>
</dependency>

2.1 生成JWT

public class JwtTest 
    // 秘钥,你可以随便取,可以取的难一点
    public static final String SECRET = "ASD!@#F^%A";

    @Test
    public void testTokenCreate() 
        HashMap<String, Object> headers = new HashMap<>();
        // 过期时间,60s
        Calendar expires = Calendar.getInstance();
        expires.add(Calendar.SECOND, 600);

        String jwtToken = JWT.create()
                // 第一部分Header
                .withHeader(headers)
                // 第二部分Payload
                .withClaim("userId", 20)
                .withClaim("userName", "LJJ")
                .withExpiresAt(expires.getTime())
                // 第三部分Signature
                .sign(Algorithm.HMAC256(SECRET));
        System.out.println(jwtToken);
    

结果如下:看红圈的两个"."符号,可见JWT的格式和第一章节描述的相吻合。

那么拿到令牌了,我们接下来就是根据令牌去解析数据。

2.2 解析JWT

注意:这里你要把2.1节生成的token自己复制一下,贴到代码里面。

@Test
public void testReadJWT() 
    // 创建一个验证的对象
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
    DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6IkxKSiIsImV4cCI6MTY2NzcyMTMzMiwidXNlcklkIjoyMH0.AJA88B0F-zKEJUCIGp9kSx2TlbpoyH88GzEn-9xN5XI");
    System.out.println(verify.getClaim("userId").asInt());
    System.out.println(verify.getClaim("userName").asString());
    System.out.println("过期时间:" + verify.getExpiresAt());

结果如下:

2.3 常见的异常

这里贴出4个常见的异常,如果JWT校验不通过,就会抛出异常:

  • SignatureVerificationException:签名不一致。
  • TokenExpiredException:令牌过期。
  • AlgorithmMismatchException:算法不匹配异常。
  • InvalidClaimException:失效的Payload异常。一般存在于这种情况:获取token的服务器比使用token的服务器时钟快,请求分发到时间慢的服务器上导致token还没生效。

JAVA实现JWT

JWT介绍

详情访问官网https://jwt.io/

JWT是什么

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

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

JWT使用场景

授权:JWT 最常见的场景。用户登录后,每个后续请求都将包括 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是目前广泛使用 JWT 的一种功能,因为它的开销很小,并且能够轻松地跨不同的域使用。

信息交换:JWT 是在各方之间安全传输信息的好方法。因为 JWT 可以签名,例如,使用公钥/私钥对,您可以确保发送者是他们所说的人。此外,由于签名是使用报头和有效载荷计算的,因此您还可以验证内容没有被篡改。

JWT结构

JWT由三部分组成,由点(.)分隔,它们是:

  • Header
  • Payload
  • Signature

因此,JWT通常如下所示。

xxxxx.yyyyy.zzzzz

Header

header 通常包含两部分:令牌类型(JWT)和使用的签名算法(如 HMAC SHA256 或 RSA)。
例如:


  "alg": "HS256",
  "typ": "JWT"

然后,对这个 JSON 进行 Base64Url 编码,形成 JWT 的第一部分。

Payload

token 的第二部分是 payload,其中包含声明。声明是关于实体(通常是 User)和其他数据的声明。声明有三种类型:registered, public, and private claims。

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

Signature

要创建签名部分,您必须获取 header 的编码、payload 的编码、secret、
在header 指定算法,并对其进行签名。

例如,如果要使用 HMAC SHA256 算法,则将按以下方式创建签名:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证消息在发送过程中没有发生更改,如果是使用私钥签名的令牌,它还可以验证 JWT 的发送者是它所说的人。

Putting all together

输出是三个由点分隔的 Base64 URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更加紧凑。

下面显示了一个JWT,它对前一个 header 和 payload 编码,并用一个 secret 进行了签名。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JAVA JWT

这里使用JWT官方推荐的库 com.auth0 / java-jwt / 3.x.x 简单实现JWT
jdk:1.8 及以上
详情访问官方网址https://github.com/auth0/java-jwt

Maven

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.19.1</version>
</dependency>

JwtOperator 操作类

实现生成token,验证token,获取payload信息

public class JwtOperator 

    //密钥
    private String secretKey = "firefly";

    //过期时间
    private Long expirationTimeInSecond = 1000L * 60 * 30;

    //header
    private Map<String, Object> headerClaims = new HashMap<>();

    //payload
    private Map<String, Object> payloadClaims = new HashMap<>();

    private static final JwtOperator jwtOperator = new JwtOperator();

    private JwtOperator()

    public static JwtOperator getInstance()
        return jwtOperator;
    

    public JwtOperator withSecretKey(String secretKey)
        this.secretKey = secretKey;
        return this;
    

    public JwtOperator withTimeOut(Long expirationTimeInSecond)
        this.expirationTimeInSecond = expirationTimeInSecond;
        return this;
    

    public JwtOperator withHeader(Map<String, Object> headerClaims)
        this.headerClaims = headerClaims;
        return this;
    

    public JwtOperator withPayload(Map<String, Object> payloadClaims)
        this.payloadClaims = payloadClaims;
        return this;
    

    /**
     * 创建token
     * @return
     */
    public String createToken()
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        String token = JWT.create()
                .withExpiresAt(new Date(System.currentTimeMillis() + expirationTimeInSecond))
                //Header
                .withHeader(headerClaims)
                //Payload
                .withPayload(payloadClaims)
                //Signature
                .sign(algorithm);
        return token;
    

    /**
     * 校验token
     * @param token
     * @return
     */
    public Boolean verifyToken(String token)
        try 
            getDecodedJWT(token);
         catch (JWTVerificationException exception)
            return false;
        
        return true;
    
    
    /**
     * 获取 Header Claims
     * @param token
     * @param claimName
     * @return
     */
    public Claim getHeaderClaims(String token, String claimName)
        DecodedJWT jwt = getDecodedJWT(token);
        return jwt.getHeaderClaim(claimName);
    

    /**
     * 获取 Payload Claims
     * @param token
     * @return
     */
    public Map<String, Claim> getPayload(String token)
        DecodedJWT jwt = getDecodedJWT(token);
        Map<String, Claim> claims = jwt.getClaims();
        return claims;
    

    /**
     * 获取 Payload Claims
     * @param token
     * @param claimsName
     * @return
     */
    public Claim getPayloadClaims(String token, String claimsName)
        DecodedJWT jwt = getDecodedJWT(token);
        Claim claim = jwt.getClaim(claimsName);
        return claim;
    

    /**
     * 解码
     * @param token
     * @return
     */
    private DecodedJWT getDecodedJWT(String token) 
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        return verifier.verify(token);
    


测试JwtOperator

public class JwtOperatorTest 

    public static void main(String[] args) throws InterruptedException 

        Map<String, Object> header = new HashMap<>();
        header.put("h1", "h1 value");
        header.put("h2", "h2 value");

        Map<String, Object> payload = new HashMap<>();
        payload.put("p1", "p1 value");
        payload.put("p2", "p2 value");

        Map<String, Object> body = new HashMap<>();
        body.put("username", "zhangsan");
        body.put("age", 18);

        payload.put("user", body);

        String token = JwtOperator.getInstance()
                .withHeader(header)
                .withSecretKey("test")
                .withTimeOut(1000L)
                .withPayload(payload)
                .createToken();

        System.out.println(token);

        Boolean isLogined = JwtOperator.getInstance().verifyToken(token);
        System.out.println(isLogined);

        System.out.println("payload: "+ JwtOperator.getInstance().getPayload(token));
        System.out.println("payload-user: "+ JwtOperator.getInstance().getPayloadClaims(token, "user"));

    


打印结果

eyJoMSI6ImgxIHZhbHVlIiwiaDIiOiJoMiB2YWx1ZSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwMSI6InAxIHZhbHVlIiwicDIiOiJwMiB2YWx1ZSIsImV4cCI6MTY1MDc5MTEyMywidXNlciI6eyJhZ2UiOjE4LCJ1c2VybmFtZSI6InpoYW5nc2FuIn19.03lYpMjJYFQ3am532iPiYm222Y-WPMrVvQCYinImBOE
true
payload: p1="p1 value", p2="p2 value", exp=1650791123, user="age":18,"username":"zhangsan"
payload-user: "age":18,"username":"zhangsan"

以上是关于Java - JWT的简单介绍和使用的主要内容,如果未能解决你的问题,请参考以下文章

面试官:jwt 是什么?java-jwt 呢?懵逼了。。

面试官:jwt 是什么?java-jwt 呢?懵逼了。。

JAVA实现JWT

Java来回顾下JWT知识点,了解一下什么是JWT?

关于bean.java的简单介绍!

JWT-JAVA简单测试用例