本文最后更新于 2024-04-01,欢迎来到我的Blog! https://www.zpeng.site/

JWT

1.简介

https://jwt.io/

https://jwt.io/introduction

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用 JSON 对象在各方之间安全地传输信息。此信息是经过数字签名的,因此可以验证和信任。

利用 Token 进行登录验证的步骤:

  1. 用户输入账号密码点击登录

  2. 后台收到账号密码,验证是否合法用户

  3. 后台验证是合法用户,生成一个 Token返回给用户

  4. 用户收到该 Token 并将其保存在每次请求的请求头中

  5. 后台每次收到请求都去查询请求头中是否含有正确的 Token,只有 Token 验证通过才会返回请求的资源。

这种基于 Token 的认证方式相比较于基于传统的 cookie 和 session 方式更加节约资源,并且对移动端和分布式系统支持更加友好,其优点有:

  • 支持跨域访问:cookie 是不支持跨域的,而 Token 可以放在请求头中传输

  • 无状态:Token 自身包含了用户登录的信息,无需在服务器端存储 session

  • 移动端支持更好:当客户端不是浏览器时,cookie 不被支持,采用 Token 无疑更好

  • 无需考虑 CRSF:不使用 cookie,也就无需考虑 CRSF 的防御

而 JWT 就是上述 Token 的一种具体实现方式,其本质就是一个字符串 是将用户信息存储到 JSON 中然后经过编码得到的字符串

2.为什么用JWT

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

传统的session认证有如下的问题:

每个用户经过我们的应用认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证用户的增多,服务器内存开销会明显增大;

用户认证之后,服务端使用session保存认证信息,那么要取到认证信息,只能访问同一台服务器,才能拿到授权的资源。这样在分布式应用上,就需要实现session共享机制,不方便集群应用;

因为session是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

自此, 可以看到了JWT认证的优势:

简洁,可以通过URL、POST参数或Http  header发送,因为数据量小,传输速度快;

自包含,负载(属于JWT的一部分)中包含了用户所需要的信息,不需要在服务器端保存会话信息,不占服务器内存,也避免了多次查询数据库,特别适用于分布式微服务;

.因为token是以json加密的形式保存在客户端的,所以JWT可以跨语言使用,原则上任何WEB形式都支持。

3.结构

JWT 由三部分组成,分别是 Header(头部)、Payload(有效载荷)、Signature(签名),用点(.)将三部分隔开便是 JWT 的结构,形如xxxxx.yyyyyy.zzzzz的字符串。

例子:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiamF2YSIsInB3ZCI6IjEyMyIsImV4cCI6MTcxMTM0NTU3Mn0.KPy2K8qYyp9xaKwXDwQUx8ohpGr11TWoy85F67GI-L4

3.1 Header

JWT Header 由两部分组成,是一个描述 JWT 元数据的JSON对象,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为HS256);typ 属性表示 Token 的类型,统一写为 JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存

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

3.2 Payload

payload 是 JWT 的主体部分,保存实体(通常是用户)信息,每一个字段就是一个 claim(声明),JWT 为我们提供了一些默认字段

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

我们也可以自定义私有字段,比如用来保存用户信息

{
    "name": "java",
    "pwd": "123",
    "exp": 1711345572
}

payload 字段会使用 BaseUrl 编码成字符串组成 JWT 的第二个部分

3.3 Signature

签名部分是对上面两部分数据签名,需要使用 base64 编码后的 header 和 payload 数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名

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

3.使用

3.1JWT的环境搭建

在pom.xml文件中导入依赖:

        <!--   JWT依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.13.0</version>
        </dependency>

        <!--        添加web启动器坐标-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

3.2创建JWT工具类Util

JWT的创建

 public String creatJWT(Map<String,String> map){
        //设置Jwt的超时时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE,30);
        //创建JWT,使用JWT.creat()方法进行创建
        //此时builder对象中默认设置了header--表头,默认为jwt
        JWTCreator.Builder builder = JWT.create();
        //将信息写入有效载荷中payload
        for (String key:map.keySet()){
            //通过withClaim方法传入传输到payload中
            builder.withClaim(key,map.get(key));
        }
        //利用builder和签名创健token,同时利用Calender类设置过期时间
        String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256("lovo"));
        return token;
    }

JWT的解码

    /**
     * 通过token和键解码信息
     * @param token
     * @param key
     * @return
     */
    public String verify(String token,String key){
        //创建解码对象,利用JWT签名去完成解码对象的创建
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("lovo")).build();
        //包含了JWT解码信息
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        //利用键得到存放在有效负载payload的数据
        String value = decodedJWT.getClaim(key).asString();
        return value;
    }

工具类测试

     @Test
    void contextLoads() {
        Util util = new Util();
        Map map = new HashMap();
        map.put("name","tom");
        map.put("pwd","123");
        System.out.println(util.creatJWT(map));

        String token = util.creatJWT(map);
        String value = util.verify(token,"pwd");
    }

3.3书写控制类Controller,用于JWT的发送

@Controller

 @Controller
class JWTController {


    @GetMapping("/jwt")
    public String greeting(Model model) {
       // model.addAttribute("name", "World");
        return "jwt"; // 对应src/main/resources/templates/greeting.html
    }

    /**
     * 模拟登录
     *
     * @param name
     * @param pwd
     * @param response
     * @return
     */
    @RequestMapping("login")
    @ResponseBody
    public String login(String name, String pwd, HttpServletResponse response) {
        if (name.equals("java") && pwd.equals("123")) {
            //此时拥有该用户,则利用该用户的信息创建JWT
            Map map = new HashMap();
            map.put("name", name);
            map.put("pwd", pwd);
            String token = new Util().creatJWT(map);
            //通过响应头发送给客户端
            response.setHeader("token", token);
            return "ok";
        }
        return "no";
    }

    @RequestMapping("getLogin")
    @ResponseBody
    public String getLogin(HttpServletRequest request) {
        String token = request.getHeader("token");
        //利用工具对其进行解码
        String name = new Util().verify(token, "name");
        String pwd = new Util().verify(token, "pwd");
        return "name=" + name + "&pwd=" + pwd;
    }

}

3.4在resource目录下书写jwt.html,用于JWT的接收

客户端通过axios接收响应头,并保存在sessionStorage中。

客户端会再次请求,读取localStorage的JWT信息,以请求头的方式,发送给服务器。

服务器从请求头中得到jwt字符串,解析后,得到数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

</head>
<body>
<form action="#">
    用户名:<input type="text" id="name"><br>
    密码:<input type="text" id="pwd"><br>
    <input type="button" value="登录" onclick="login()">
    <input type="button" value="获取登录对象" onclick="getLogin()">
</form>

<script>
    function login(){
        let nameObj = document.getElementById("name")
        let pwdObj = document.getElementById("pwd")
        axios.get('/login',{
            params:{
                name:nameObj.value,
                pwd:pwdObj.value
            }
        }).then(res=>{
            console.log(res)
            if (res.data==='ok'){
                localStorage.setItem("token",res.headers.token)
            }else {
                alert("用户名或密码错误!")
            }
        })
    }


    /**
     * 获取登录对象
     */
    function getLogin(){
        //读取localStorage的信息,以请求头的方式,发送给服务器
        let token = localStorage.getItem("token");
        //添加配置,在请求头中加入JWT的信息
        let config = {
            headers:{"token":token}
        }
        axios.get('/getLogin',config).then(res=>{
            console.log(res)
        })
    }
</script>

</body>
</html>

4.测试

启动springboot项目

访问http://localhost:8080/jwt