什么是JWT

一、基于传统的Session认证

传统Session认证原理

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

image

  1. 客户端A向服务器第一次向服务器发送请求后,服务器会为客户端创建一个Session会话。并生成一个只属于客户端A的令牌token,并将token返回给客户端A。
  2. 客户端A拿到token后,以后每一次请求服务器,就会将token给服务器,服务器通过token去识别你是哪一个客户,

暴露的问题

  1. 每个用户经过我们的认证之后,我们的应用都要在服务器做一次记录,以方便用户在下一次请求进行鉴别。通常,session都会保存到内存中,而随着认证用户的增多,服务端的开销会明显增大。
  2. 用户认证后,服务端做认证记录,如果认证的记录被保存到内存中,这就意味着用户下次请求还必须要请求在同一台服务器上,这样才能够拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能力,限制了应用的扩展能力
  3. 因为是基于cookie进行用户识别的,如果cookie被黑客拦截并捕获,用户就会容器受到跨站请求伪造的攻击
  4. sessionId就是一个特征值,表达的信息不够丰富,不容器扩展。

二、基于JWT认证

image-1677662918214

认证原理

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

JWT兼容更多的客户端

传统的HTTPSession依赖浏览器的Cookie存放SessionId,所以要求客户端必须是浏览器。现在的JavaWeb系统,客户端可以是浏览器、App、小程序、以及物联网设备。为了让这些设备都能访问到JavaWeb项目,就必须引入JWT技术。JWT的Token是纯字符串,至于客户端怎么保存,没有具体要求。只要客户端发起请求的时候,附带上Token即可。所以像物理网设备,我们可以用sqlite存储token数据。

JWT结构

JWT分为3个部分:Header、Payload、Signature

header.payload.Signature
Base64{xxx}.Base64{xxx}.{xxx|

JWT结构 说明
Header 表头通常2个部分组成:令牌的类型(JWT)和所使用的签名算法,如HMAC、RSA等。他会使用Base64编码组成JWT结构的第一部分
Payload 有效负载,其中包含声明。声明是有关用户的非敏感数据和其他数据的声明。同样的他也会使用Base64进行编码,组成JWT结构的第二部分
Signature 编码后的header+编码后的payload+密钥

JWT的简单使用

一、导入依赖

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

二、获取JWT令牌

@Test
public void getToken() {
    Map<String, Object> map = new HashMap<>();
    Calendar instance = Calendar.getInstance();
    instance.add(Calendar.SECOND, 2000);
    String token = JWT.create() // 创建JWT
            .withHeader(map)    // 设置Header,默认可以不写
            .withClaim("userId", 21)    // 设置payload
            .withClaim("username", "yjiewei") // 设置payload
            .withExpiresAt(instance.getTime()) // 指定令牌的过期时间
            .sign(Algorithm.HMAC256("!Rdsafgs&(*)R")); // 签名,自己记住
    System.out.println(token);
}

三、认证令牌

拿到客户端传过来的令牌,进行认证,如果抛异常,说明认证不通过。

@Test
public void  tokenVerify() {
    String token = "eyJ0eXAiOiJKV1QiLCI1NiJ9.eyJleHAiOjE2MjkppZXdlaSJ9.e4auEPHvhunCnQ";
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!Rdsafgs&(*)R")).build();
}

JWT的工具类封装

public class JWTUtil{
    // 指定一个签名(秘钥),签名一定要保存好,不能泄露出去
    private static String SECRET = "privatekey#^&^%!save";
    
    /**
    *	通过传入 header.payload.sign 生成密钥
    */
    public static String getToken(Map<String, String> map) {
        JWTCreator.Builder builder = JWT.create();
        // 传入header 自动生成
        // 传入payload 用户信息
        // map.forEach(builder::withClaim);
        map.forEach((k, v) -> {
            build.withClaim(k ,v);
        })
        
        // 日期对象, Calendar.DATE 表示天
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 3); //默认3天过期
        
        builder.withExpiresAt(instance.getTime());
        
        // 采用HMAC256算法 传入sign签名
        return builder.sign(Algorithm.HMAC256(SECRET));
    }
    
    /**
    *  获取token中的payload(用户信息)
    */
    public static DecodedJWT verify(String token) {
    	return JWT.require(Algorithm.HMAC256(SECRET))
           			.build()
           			.verify(token)
           			.getClaims();
    }
}