一篇文章学会JSON Web Token (JWT)

 

官网:JSON Web Tokens - JWT.io

一、简介

1.JWT是什么?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

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

通俗的说:JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。广义上讲JWT是一个标准的名称;狭义上讲JWT指的就是用来传递的那个token字符串。

2.JWT做什么?

  • 1.授权

    这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

    在这种场景下,一旦用户完成了登陆,在接下来的每个请求中都包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。

  • 2.信息交换

    JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

    在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

3.传统基于Session认证

由于Http协议本身是一种无状态、短连接的协议,当客户端用户每向后端服务器发一次请求就是一次全新的请求,之间没有任何关联关系,那么后端服务器就无法进行分辨哪些请求是由同一个客户端用户发起的,所以为了让后端应用能识别是哪个用户发出的多次请求,即让同一客户端用户发起的多次请求之间有关系,以达到与后端进行会话的目的,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为Cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于Session认证。

但是这种基于Session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于Session认证应用的问题就会暴露出来:

  • Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
  • 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
  • CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

4.基于Token认证

基于Token的鉴权机制,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)
  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  • 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
  • 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  • 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

简化流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

5.JWT的特点优势

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
  • 不需要在服务端保存会话信息,特别适用于分布式微服务。
  • 因为JSON的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

6.JWT的结构是什么?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。

JWT的头部承载两部分信息:

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

    JWT里验证和签名使用的算法如下:

    JWS 算法名称 描述
    HS256 HMAC256 HMAC with SHA-256
    HS384 HMAC384 HMAC with SHA-384
    HS512 HMAC512 HMAC with SHA-512
    RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
    RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
    RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
    ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
    ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
    ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

    使用代码如下

    // header Map
    Map<String, Object> map = new HashMap<>();
    map.put("alg", "HS256");
    map.put("typ", "JWT");
    

生成的header:

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

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

 

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playload

载荷就是存放有效信息的地方。这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

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

  • iss: JWT签发者
  • sub: JWT所面向的用户
  • aud: 接收JWT的一方
  • exp: JWT的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该JWT都是不可用的.
  • iat: JWT的签发时间
  • jti: JWT的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "10001",
  "name": "YJS",
  "admin": true
}

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature

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

  • header (base64处理后的)
  • payload (base64处理后的)
  • secret

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

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。

二、快速使用

1.引入依赖

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

2.生成Token


	/**
     * 生成Token
     * @param playload 传入载荷信息
     * @return
     */
public static String generalToken(Map<String,String> playload){
    JWTCreator.Builder builder = JWT.create();
    //        playload.forEach((k,v)->{
    //            builder.withClaim(k,v);
    //        });
    playload.forEach(builder::withClaim);  // 等价于上面注释写法

    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.SECOND, 60);

    builder.withExpiresAt(calendar.getTime());  // 设置超时时间
    String SIGNATURE= "I#^%$M54Y%$#J^%$S0612";
    return builder.sign(Algorithm.HMAC256(SIGNATURE)).toString();
}


public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    map.put("userId", "1001");
    map.put("userName", "admin");

    System.out.println(JwtUtil.generalToken(map));
    // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
    // eyJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjUwNzcxOTM5LCJ1c2VySWQiOiIxMDAxIn0.
    // axCdqnMEId3jl2gfcE8sCBJ6AL1ugFzq7FmFXV8oJVU
}

3.根据令牌和签名解析数据

DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
String id = verify.getClaim("userId").asString();
String name = verify.getClaim("userName").asString();
System.out.println(id + name);

4.验证token是否过期

	/**
     * 验证token是否过期
     * @param token 从客户端传递的token
     * @return 过期:true
     */
public static boolean verify(String token){
    DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
    Date expiresDate = verify.getExpiresAt();
    return expiresDate.getTime() > new Date().getTime();
}

5.常见异常信息

  • SignatureVerificationException: 签名不一致异常
  • TokenExpiredException: 令牌过期异常
  • AlgorithmMismatchException: 算法不匹配异常
  • InvalidClaimException: 失效的payload异常

三、封装工具类

package cn.imyjs.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

/**
 * @Classname JwtUtil
 * @Description TODO
 * @Date 2022/4/24 11:27
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */
public class JwtUtil {
    private static final String SIGNATURE= "I#^%$M54Y%$#J^%$S0612";
    /**
     * 生成Token
     * @param playload 传入载荷信息
     * @return
     */
    public static String generalToken(Map<String,String> playload){
        JWTCreator.Builder builder = JWT.create();
        playload.forEach(builder::withClaim);  

        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 60);

        builder.withExpiresAt(calendar.getTime());  // 设置超时时间
        return builder.sign(Algorithm.HMAC256(SIGNATURE)).toString();
    }

    /**
     * 获取token中payload
     * @param token 从客户端传递的token
     * @return DecodedJWT
     */
    public static DecodedJWT getToken(String token){
        return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
    }


    /**
     * 验证token
     * @param token
     * @return
     */
    public static void verify(String token){
        JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
    }
}

四、模拟登录验证

1.模拟登录接口

@RestController
@RequestMapping("/user")
public class UserController {
    // 模拟数据库数据
    private static final String USERNAME = "admin";
    private static final String PASSWORD = "123456";

    @GetMapping("/login/{uname}/{pwd}")
    public Map<String,Object> login(@PathVariable String uname, @PathVariable String pwd){
         Map<String, Object> result = new HashMap<>();
        if (USERNAME.equals(uname) && PASSWORD.equals(pwd)){
            Map<String, String> map = new HashMap<>();
            map.put("username", uname);
            // 登录成功
            String token = JwtUtil.generalToken(map);
            result.put("code",200);
            result.put("token",token);
            result.put("msg","登录成功");
            return result;
        }else {
            result.put("code",401);
            result.put("msg","身份验证失败!");
            return result;
        }
    }

}

2.模拟登录成功验证Token

@PostMapping("/test")
public Map<String, Object> test(String token) {
    Map<String, Object> map = new HashMap<>();
    try {
        JwtUtil.verify(token);
        map.put("msg", "验证通过~~~");
        map.put("state", true);
    } catch (TokenExpiredException e) {
        map.put("state", false);
        map.put("msg", "Token已经过期!!!");
    } catch (SignatureVerificationException e){
        map.put("state", false);
        map.put("msg", "签名错误!!!");
    } catch (AlgorithmMismatchException e){
        map.put("state", false);
        map.put("msg", "加密算法不匹配!!!");
    } catch (Exception e) {
        e.printStackTrace();
        map.put("state", false);
        map.put("msg", "无效token~~");
    }
    return map;
}

五、使用拦截器优化

1.定义拦截器

package cn.imyjs.config;

import cn.imyjs.utils.JwtUtil;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @Classname JwtInterceptor
 * @Description TODO
 * @Date 2022/4/24 14:25
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */
public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        Map<String,Object> map = new HashMap<>();
        try {
            JwtUtil.verify(token);
            return true;
        } catch (TokenExpiredException e) {
            map.put("state", false);
            map.put("msg", "Token已经过期!!!");
        } catch (SignatureVerificationException e){
            map.put("state", false);
            map.put("msg", "签名错误!!!");
        } catch (AlgorithmMismatchException e){
            map.put("state", false);
            map.put("msg", "加密算法不匹配!!!");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state", false);
            map.put("msg", "无效token~~");
        }
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }
}

2.配置拦截器

package cn.imyjs.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Classname JwtInterceptorConfig
 * @Description TODO
 * @Date 2022/4/24 14:28
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login/**");
    }
}

3.简化控制层代码

@PostMapping("/test")
public Map<String, Object> test(String token) {
    Map<String, Object> map = new HashMap<>();
    map.put("code", 200);
    map.put("msg", "success");
    return map;
}

六、JWT使用注意

  • 不应该在JWT的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
  • 保护好secret私钥,该私钥非常重要。
  • 建议使用https协议。

微信关注

编程那点事儿

本站为非盈利性站点,所有资源、文章等仅供学习参考,并不贩卖软件且不存在任何商业目的及用途,如果您访问和下载某文件,表示您同意只将此文件用于参考、学习而非其他用途。
本站所发布的一切软件资源、文章内容、页面内容可能整理来自于互联网,在此郑重声明本站仅限用于学习和研究目的;并告知用户不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
如果本站相关内容有侵犯到您的合法权益,请仔细阅读本站公布的投诉指引页相关内容联系我,依法依规进行处理!
作者:理想
链接:https://www.imyjs.cn/archives/730
THE END
二维码
一篇文章学会JSON Web Token (JWT)
  官网:JSON Web Tokens - JWT.io 一、简介 ……
<<上一篇
下一篇>>
文章目录
关闭
目 录