JWT入门笔记

JWT引言

image-20210408192729820

何为 JWT

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

通俗的来说,JWT就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

JWT 能做什么

授权

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

信息交换

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

注意:JWT 跟 Session 不一样, JWT 存储在客户端, Session 存储在服务器端,服务器断电后 Session 就没了,而 JWT 因为存储在客户端,所以就不会被影响,只要 JWT 不过期,就可以继续使用。

为什么是 JWT

认证方式

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

认证流程

在这里插入图片描述

暴露问题

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

在这里插入图片描述

基于 JWT 验证

在这里插入图片描述

验证流程

首先,前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过 SSL 加密的传输(Https 协议),从而避免敏感信息被嗅探。

后端核对用户名和密码成功后,将用户的 id 等其他信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成一个JWT(Token)。形成的 JWT 就是一个形同 XXX.YYY.ZZZ 的字符串。 token -> Head.Payload.Signature

后端将 JWT 字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在 localStorage 或 sessionStorage 上,退出登录时前端删除保存的 JWT 即可。

前端在每次请求时将 JWT 放入 HTTP Header 中的 Authorization 位。(解决XSS和XSRF问题)

后端检查是否存在,如存在验证 JWT 的有效性。例如,检查签名是否正确;检查 Token 是否过期;检查 Token 的接收方是否是自己(可选)。

验证通过后后端使用 JWT 中包含的用户信息进行其他逻辑操作,返回相应结果。

JWT 优势

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快

  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

  • 不需要在服务端保存会话信息,特别适用于分布式微服务。

JWT的结构

令牌组成

  1. 标头(Header)
  2. 有效载荷(Payload)
  3. 签名(Signature)

因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz —> Header.Payload.Signature

标头通常由两部分组成:令牌的类型(即 JWT )和所使用的签名算法,例如 HMAC SHA256 或 RSA 。

它会使用 Base64 编码组成 JWT 结构的第一部分。

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

Payload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。

同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

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

Signature

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 Header 和 Payload 以及我们提供的一个密钥,然后使用 Header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

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

签名目的

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

信息安全问题

  • 在这里大家一定会问一个问题:Base64 是一种编码,是可逆的,那么我的信息不就被暴露了吗?

  • 是的。所以,在 JWT 中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的 User ID 。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在 JWT 中了。如果将用户的密码放在了 JWT 中,那么怀有恶意的第三方通过 Base64 解码就能很快地知道你的密码了。因此 JWT 适合用于向 Web 应用传递一些非敏感信息。JWT 还经常用于设计用户认证和授权系统,甚至实现 Web 应用的单点登录。

组合在一起

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

  • 简洁(Compact)
    可以通过 URL , POST 参数或者在 HTTP Header 发送,因为数据量小,传输速度快

  • 自包含(Self-contained)
    负载中包含了所有用户所需要的信息,避免了多次查询数据库

Encoded JWT

JWT.io Debugger

JWT 的简易实现

引入依赖

<!--引入jwt-->
   <dependency>
       <groupId>com.auth0</groupId>
       <artifactId>java-jwt</artifactId>
       <version>3.14.0</version>
   </dependency>

生成 token

@Test
void GenerateToken() {
    // 创建token
    HashMap<String, Object> map = new HashMap<>();

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


    String token = JWT.create()
            .withHeader(map) // Header
            .withClaim("userId", 21) // Payload
            .withClaim("userName", "Pluto")
            .withExpiresAt(instance.getTime())
            .sign(Algorithm.HMAC256("!QE$&SR"));// Sign

    System.out.println(token);

}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. -----> Header
eyJ1c2VyTmFtZSI6IlBsdXRvIiwiZXhwIjoxNjE3ODg3MDAyLCJ1c2VySWQiOjIxfQ. -----> Payload
FQQmT32BN1rwhsH_tp8j-KWFCOTEvx7-fl1pAN23CR0 -----> Signature

解析 token

@Test
public void ResolveToken() {
// 创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!QE$&SR")).build();

DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6IlBsdXRvIiwiZXhwIjoxNjE3ODg3MDAyLCJ1c2VySWQiOjIxfQ.FQQmT32BN1rwhsH_tp8j-KWFCOTEvx7-fl1pAN23CR0");

System.out.println(verify.getClaim("userId"));
System.out.println(verify.getClaim("userName"));
System.out.println("过期时间:" + verify.getExpiresAt());

}

image-20210408210918795

常见异常信息

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

JWT 简单整合Spring boot

封装工具类

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

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

/**
 * JWT工具类
 *
 * @author PlutoWu
 * @date 2021/04/08
 */
public class JWTUtils {

    private static final String Signature = "!QE$&SR12Sd23";

    /**
     * 获得令牌
     * Header.Payload.Sign
     *
     * @param map
     * @return {@link String}
     */
    public static String getToken(Map<String, String> map) {

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7); // 默认7天过期

        // 创建JWT builder
        JWTCreator.Builder builder = JWT.create();

        // Payload
        map.forEach( (k, v)-> {
            builder.withClaim(k,v);
        } );

        // Signature
        String token = builder.withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(Signature));

        return token;

    }

    /**
     * 验证token
     * 合法性
     * @param token 令牌
     */
    public static void verify (String token) {

        JWT.require(Algorithm.HMAC256(Signature)).build().verify(token);

    }

    /**
     * 得到令牌信息
     *
     * @param token
     * @return {@link DecodedJWT}
     */
    public static DecodedJWT getTokenInfo (String token) {

        DecodedJWT verify = JWT.require(Algorithm.HMAC256(Signature)).build().verify(token);
        return verify;

    }

}

完善依赖

<!--引入jwt-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.14.0</version>
</dependency>

<!--引入mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

<!--引入lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!--引入druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.19</version>
</dependency>

<!--引入mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

编写配置

server.port=8989
spring.application.name=jwt

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root

mybatis.type-aliases-package=cn.pluto.springboot_jwt.entity
mybatis.mapper-locations=classpath:cn/pluto/mapper/*.xml

logging.level.com.baizhi.dao=debug

数据库设计

在这里插入图片描述

CREATE DATABASE jwt;
USE jwt;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(80) DEFAULT NULL COMMENT '用户名',
  `password` varchar(40) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

Entity

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 用户
 *
 * @author PlutoWu
 * @date 2021/04/08
 */
@Data
@Accessors(chain=true)
public class User {

    private String id;
    private String name;
    private String password;

}

DAO

import cn.pluto.springboot_jwt.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author PlutoWu
 * @date 2021/04/08
 */
@Mapper
public interface UserDAO {

    User login(User user);

}
<?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="cn.pluto.dao.UserDAO">
    <!--这里就写的简单点了毕竟不是重点-->
    <select id="login" parameterType="User" resultType="User">
        select * from user where name=#{name} and password = #{password}
    </select>
</mapper>

Service

import cn.pluto.springboot_jwt.entity.User;

/**
 * 用户服务
 *
 * @author PlutoWu
 * @date 2021/04/08
 */
public interface UserService {

    User login(User user); // 登录接口

}
import cn.pluto.springboot_jwt.dao.UserDAO;
import cn.pluto.springboot_jwt.entity.User;
import cn.pluto.springboot_jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public User login(User user) {

        User userDB = userDAO.login(user);

        if(userDB!=null){
            return userDB;
        }
        throw new RuntimeException("登录失败~~");
    }
}

Controller

import cn.pluto.springboot_jwt.entity.User;
import cn.pluto.springboot_jwt.service.UserService;
import cn.pluto.springboot_jwt.utils.JWTUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
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 javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @author PlutoWu
 * @date 2021/04/08
 */
@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;
    }

    @PostMapping("/user/test")
    public Map<String, Object> test(HttpServletRequest request) {

        Map<String, Object> map = new HashMap<>();

        String token = request.getHeader("token");
        DecodedJWT verify = JWTUtils.verify(token);

        log.info("用户id:[{}]",verify.getClaim("id").asString());
        log.info("用户name:[{}]",verify.getClaim("name").asString());

        map.put("msg", "验证通过~~~");
        map.put("state", true);
        return map;
    }


}

Interceptor

import cn.pluto.springboot_jwt.utils.JWTUtils;
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;

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 {
            JWTUtils.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~~");
        }
        // 将 map 转化成 json Jackson
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }

}

Config

import cn.pluto.springboot_jwt.interceptors.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器配置
 *
 * @author PlutoWu
 * @date 2021/04/08
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor()).
                excludePathPatterns("/user/login")  // 放行
                .addPathPatterns("/user/test"); // 拦截除了"/user/**的所有请求路径
    }
}

测试

测试接口login,填写正确的用户名与密码返回 token

image-20210408222115281

通过在 request 的 Headers 加入刚才登录成功获取的 token 得到成功验证

image-20210408222221055

至此,测试完毕,token 能正常的执行其功能,搭建的简易测试成功。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!