Jwt简介

JSON Web Token(缩写 JWT),本文介绍它的原理和用法。

JSON Web Token 简称JWT,是目前最流行的跨域认证解决方案,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

Jwt的作用

  1. 授权
    这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
  2. 信息交换
    JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

一般系统的认证流程

  1. 用户向服务器发送用户名和密码。
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个 session_id,写入用户的Cookie
  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

JWT - 图1

问题所在

这种模式的问题在于,扩展性不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT认证流程

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

  1. {
  2. "姓名": "宋祖儿",
  3. "角色": "admin",
  4. "到期时间": "2022-03-04"
  5. }

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT - 图2

JWT组成结构

JWT 的三个部分依次如下

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

JWT - 图3

组成如下

  1. Header.Payload.Signature
  1. Header
    Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
  1. {
  2. "alg": "HS256",
  3. "typ": "JWT"
  4. }

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

  1. Payload
    Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

  1. {
  2. "sub": "1234567890",
  3. "name": "John Doe",
  4. "admin": true
  5. }

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

  1. Signature
    Signature 部分是对前两部分的签名,防止数据篡改。
    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
  1. HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

  1. Authorization: token

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

  1. http://localhost:8081/test/test?name=songzuer&password=521521&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleddfsaafHAiOjE2NDYzNzE5MTgsInVzZXJuYW1lIjoic29uZ3p1ZXIifQ.VKlDAxnuUwwDfuX5xDlGMeJODqO0EaQ30A9Z0zuii8A

简单使用

  1. 引入依赖
  1. <!--引入jwt-->
  2. <dependency>
  3. <groupId>com.auth0</groupId>
  4. <artifactId>java-jwt</artifactId>
  5. <version>3.4.0</version>
  6. </dependency>
  1. 生成Token
  1. @Test
  2. public void generateToken(){
  3. Calendar instance = Calendar.getInstance();
  4. instance.add(Calendar.SECOND, 90);
  5. //生成令牌
  6. String token = JWT.create()
  7. .withClaim("username", "songzuer")//设置自定义用户名
  8. .withExpiresAt(instance.getTime())//设置过期时间
  9. .sign(Algorithm.HMAC256("token!SZR1Q#C$RB"));//设置签名 保密 复杂
  10. //输出令牌
  11. System.out.println(token);
  12. }

输出

  1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDYzNjMzNDMsInVzZXJuYW1lIjoic29uZ3p1ZXIifQ.QBomDWeC6B3W4c9Gl_ud3fFqv62HSs0t9v-UDUxTsHU
  1. 根据令牌和签名解析数据
  1. @Test
  2. public void analysisToken(){
  3. String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDYzNjMzNDMsInVzZXJuYW1lIjoic29uZ3p1ZXIifQ.QBomDWeC6B3W4c9Gl_ud3fFqv62HSs0t9v-UDUxTsHU";
  4. JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!SZR1Q#C$RB")).build();
  5. DecodedJWT decodedJWT = jwtVerifier.verify(token);
  6. System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
  7. System.out.println("过期时间: "+decodedJWT.getExpiresAt());
  8. }
  9. //打印结果
  10. //用户名: songzuer
  11. //过期时间: Fri Mar 04 11:09:03 CST 2022
  1. 常见异常
  1. SignatureVerificationException:签名不一致异常
  2. TokenExpiredException:令牌过期异常
  3. AlgorithmMismatchException:算法不匹配异常
  4. InvalidClaimException:失效的payload异常

Spring Boot中使用JWT

  1. 创建SpringBoot项目做基本配置
  1. #配置端口号
  2. server.port=8081
  3. # 配置Tomcat编码,默认为UTF-8
  4. server.tomcat.uri-encoding=UTF-8
  5. # 配置最大线程数
  6. server.tomcat.max-threads=1000
  7. #配置数据库
  8. spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  9. spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
  10. spring.datasource.username=root
  11. spring.datasource.password=cj123456789
  12. #Mybatis配置
  13. mybatis.mapper-locations=classpath:mapper/*.xml
  14. mybatis.type-aliases-package=com.example.jwtdemo.entity
  1. 引入依赖
  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. <scope>runtime</scope>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.projectlombok</groupId>
  13. <artifactId>lombok</artifactId>
  14. <optional>true</optional>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-test</artifactId>
  19. <scope>test</scope>
  20. </dependency>
  21. <!--引入JWT-->
  22. <dependency>
  23. <groupId>com.auth0</groupId>
  24. <artifactId>java-jwt</artifactId>
  25. <version>3.4.0</version>
  26. </dependency>
  27. <!--引入mybatis-->
  28. <dependency>
  29. <groupId>org.mybatis.spring.boot</groupId>
  30. <artifactId>mybatis-spring-boot-starter</artifactId>
  31. <version>2.1.3</version>
  32. </dependency>
  33. <!--引入druid-->
  34. <dependency>
  35. <groupId>com.alibaba</groupId>
  36. <artifactId>druid</artifactId>
  37. <version>1.2.4</version>
  38. </dependency>
  39. </dependencies>
  1. 创建一张简单的用户表
  1. DROP TABLE IF EXISTS `user`;
  2. CREATE TABLE `user` (
  3. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  4. `name` varchar(80) DEFAULT NULL COMMENT '用户名',
  5. `password` varchar(40) DEFAULT NULL COMMENT '用户密码',
  6. PRIMARY KEY (`id`)
  7. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  1. 编写entity、mapper、service、controller

entity

  1. package com.example.jwtdemo.entity;
  2. import lombok.Data;
  3. import lombok.experimental.Accessors;
  4. @Data
  5. @Accessors(chain=true)
  6. public class User {
  7. private String id;
  8. private String name;
  9. private String password;
  10. }

mapper和xml

  1. package com.example.jwtdemo.mapper;
  2. import com.example.jwtdemo.entity.User;
  3. import org.apache.ibatis.annotations.Mapper;
  4. @Mapper
  5. public interface UserMapper {
  6. User login(User user);
  7. }
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.jwtdemo.mapper.UserMapper">
    <select id="login" parameterType="User" resultType="User">
        select * from user where name=#{name} and password = #{password}
    </select>
</mapper>

service和impl

package com.example.jwtdemo.service;

import com.example.jwtdemo.entity.User;

public interface UserService {
    User login(User user);//登录接口
}
package com.example.jwtdemo.service;

import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userDAO;

    @Override
    public User login(User user) {
        User userDB = userDAO.login(user);
        if(userDB!=null){
            return userDB;
        }
        throw  new RuntimeException("登录失败~~");
    }
}

controller

package com.example.jwtdemo.controller;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.service.UserService;
import com.example.jwtdemo.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/login")
    public Map<String,Object> login(User user) {
        Map<String,Object> result = new HashMap<>();
        log.info("用户名: [{}]", user.getName());
        log.info("密码: [{}]", user.getPassword());
        try {
            User userDB = userService.login(user);
            Map<String, String> map = new HashMap<>();//用来存放payload
            map.put("id",userDB.getId());
            map.put("username", userDB.getName());
            String token = JWTUtils.getToken(map);
            result.put("state",true);
            result.put("msg","登录成功!!!");
            result.put("token",token); //成功返回token信息
        } catch (Exception e) {
            e.printStackTrace();
            result.put("state","false");
            result.put("msg",e.getMessage());
        }
        return result;
    }
}

结构如下:

JWT - 图4

  1. 测试
  • 输入错误的账号密码不返回Token

JWT - 图5

  • 输入正确的账号密码返回Token

JWT - 图6

  1. 测试验证请求中的Token
    JWTUtils
package com.example.jwtdemo.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;

public class JWTUtils {
    private static String TOKEN = "token!SZR1Q#C$RB";

    /**
     * 生成token
     * @param map 传入payload
     * @return 返回token
     */
    public static String getToken(Map<String,String> map){
        JWTCreator.Builder builder = JWT.create();
        map.forEach((k,v)->{
            builder.withClaim(k,v);
        });
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,7);
        builder.withExpiresAt(instance.getTime());
        return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
    }

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

    /**
     * 获取token中payload
     * @param token
     * @return
     */
    public static DecodedJWT getToken(String token){
        return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
    }
}

测试验证Token的Controller