基于JWT的token认证

基于JWT的token认证

   一、什么是JWT?

   JWT (JSON Web Token)是目前最流行的跨域身份验证解决方案,是一种基于 Token 的认证授权机制。

   从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

  JWT架构如下:

 

   二、JWT的组成部分

   JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分。

   JWT 通常是这样的:xxxxx.yyyyy.zzzzz,示例如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

   1.  Header(头部)

   Header部分指定了该JWT使用的签名算法

   Header 通常由两部分组成: 

  • typ(Type):令牌类型,也就是 JWT。
  • alg(Algorithm):签名算法,比如 HS256。 

   示例:

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

   Json形式的Header 被 Base64Url 编码后成为 JWT 的第一部分。

   2. Payload (载荷)

   Payload部分表面了JWT的意图

   用来存放实际需要传递的数据,Json 形式的Payload 被 Base64Url 编码后成为 JWT 的第二部分。

   Claims 分为三种类型:

  • Registered Claims(注册声明):预定义的一些声明,建议使用,但不是强制性的。
  • Public Claims(公有声明):JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registry 中定义它们。
  • Private Claims(私有声明):JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。

   下面是一些常见的注册声明:

  • iss(issuer):JWT 签发方。
  • iat(issued at time):JWT 签发时间。
  • sub(subject):JWT 主题。aud(audience):JWT 接收方。
  • exp(expiration time):JWT 的过期时间
  • nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti(JWT ID):JWT 唯一标识。

   示例:

1 {
2   "uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
3   "sub": "1234567890",
4   "name": "John Doe",
5   "exp": 15323232,
6   "iat": 1516239022,
7   "scope": ["admin", "user"]
8 }

  Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

  其中有几个重要属性,iss (签发者), exp (过期时间), sub (主体), aud(受众)等。

  3.  Signature (签名)

  Signature 部分是对前两部分的签名,主要为了让JWT不能被随意篡改。生成的签名会成为 JWT 的第三部分。

  这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥(一定不要泄露出去)。
  • 签名算法。

  签名的计算公式如下:

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

   服务器通过使用 Base64URL 编码的 header 和 payload 中间用 . 隔开,再使用 header 中指定的 Hash 算法,加上密钥对这个字符串进行 Hash 得到 signature。

   我们可以通过 jwt.io 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

   如下图:

 

   这里针对签名的算法,再补充一下: 

   为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。加密的算法一般有2类:

   对称加密:secretKey指加密密钥,可以生成签名与验签

   非对称加密:secretKey指私钥,只用来生成签名,不能用来验签(验签用的是公钥)

   JWT的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK

   到目前为止,jwt的签名算法有三种:

  • HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
  • RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
  • ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)

   三、JWT的特点 

   1.  无状态

  JWT是无状态的,无状态性是指服务器端不需要存储任何关于用户会话或身份验证状态的信息。因此,服务器只需要验证JWT的签名和有效期来验证令牌的真实性和有效性,并据此授权用户的访问请求。

  2. 跨端 / 跨域支持

  由于JWT的无状态性,使其非常适用于分布式系统和跨服务的身份验证场景。每个服务都可以独立地验证和解析JWT,无需进行共享或同步状态。同时由于不依赖Cookie等特性,在客户端中也可以使用JWT。

  3. 拓展性强

  JWT的Payload部分可以添加任意所需信息,通过解密,服务端可以完整的拿到加密前的数据,可以包括用户的id、username等任意字段。

  4. 长度无限制

  由于Payload部分可以添加任意JSON格式的数据,因此JWT的长度很可能会超过限制,导致溢出报错。

  5. 难以主动失效

  正因为JWT的无状态性,想主动使其失效变得非常困难。通常JWT会设置过期时间exp,但是有时我们需要主动令其失效,例如在用户修改了密码之后。一般的做法是设置黑名单,将失效的token信息维护在黑名单中,但是这与JWT的无状态性背驰,算是一个痛点。

  四、如何基于 JWT 进行身份验证?

  在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。

  1. 登录认证流程

  1)用户登录,输入账号密码

  2)服务端接受登录请求,常规校验(验证码),根据用户信息生成token,返还客户端

  3)客户端收到登录返还信息,将得到的token保存下来,通常保存在localStorage里面

  4)再次发起请求授权资源,请求头中携带token信息

  5)服务端接受请求,拦截器中校验token信息,校验通过放行,校验不通过作相应异常提示

  五、Java中使用JWT

  1. 引入maven依赖

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

  2.  JWT的token生成和解析

  关于JWT的token生成和解析,下面针对签名算法的两种方式(对称加密和非对称加密),分别举例介绍

  1)对称签名

  生成JWT的token

    /**
     * 生成JWT token
     */
    @Test
    void generateToken(){
        //预设一个token过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR,1);//过期时间为1小时
        String token = JWT.create()
                .withHeader(new HashMap<>())//Header
                .withClaim("userId", 123)//PayLoad
                .withClaim("userName", "程")
                .withClaim("userId1", "456")
                .withExpiresAt(calendar.getTime())//过期时间
                .sign(Algorithm.HMAC256("12345"));//签名用的密钥secret
        System.out.println(token);
    }

  解析JWT字符串

    /**
     * 解析jwt字符串
     */
    @Test
    void resolveToken(){
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWRJbnQiOjEyMywidXNlcklkU3RyaW5nIjoiNDU2IiwidXNlck5hbWUiOiLnqIsiLCJleHAiOjE2NzU5MTQzMDZ9.MNC-YCxt8C0t9SNbj3_XMRknkK3-PKBP5xX6_JLB8y8";
        //创建解析对象,使用的算法和secret要和创建token时保持一致
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("12345")).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        System.out.println(decodedJWT.getPayload());//base64编码的payLoad
        Claim userIdInt = decodedJWT.getClaim("userIdInt");
        Claim userIdString = decodedJWT.getClaim("userIdString");
        Claim userName = decodedJWT.getClaim("userName");
        System.out.println("userIdInt:"+userIdInt.asInt());
        System.out.println("userIdString:"+userIdString.asString());
        System.out.println("userName:"+userName.asString());
        System.out.println("过期时间:"+new LocalDateTime(decodedJWT.getExpiresAt()));
    }

  2)非对称签名

 1 private static final String RSA_PRIVATE_KEY = "...";
 2 private static final String RSA_PUBLIC_KEY = "...";
 3 
 4     /**
 5      * 生成token
 6      * @param payload token携带的信息
 7      * @return token字符串
 8      */
 9 public static String getTokenRsa(Map<String,String> payload){
10     // 指定token过期时间为7天
11     Calendar calendar = Calendar.getInstance();
12     calendar.add(Calendar.DATE, 7);
13 
14     JWTCreator.Builder builder = JWT.create();
15     // 构建payload
16     payload.forEach((k,v) -> builder.withClaim(k,v));
17 
18     // 利用hutool创建RSA
19     RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
20     // 获取私钥
21     RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey();
22     // 签名时传入私钥
23     String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.RSA256(null, privateKey));
24     return token;
25 }
26 
27     /**
28      * 解析token
29      * @param token token字符串
30      * @return 解析后的token
31      */
32 public static DecodedJWT decodeRsa(String token){
33     // 利用hutool创建RSA
34     RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
35     // 获取RSA公钥
36     RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey();
37     // 验签时传入公钥
38     JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(publicKey, null)).build();
39     DecodedJWT decodedJWT = jwtVerifier.verify(token);
40     return decodedJWT;
41 }

  六、注意事项

  1. JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。

  2. JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用, Payload 要加入 exp (JWT 的过期时间),JWT的过期时间不宜设置过长,永久有效的 JWT 不合理。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。

  3. 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

  4. JWT 应该存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。

  5.  一定不要将隐私信息存放在 Payload 当中。

  6. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。

  7. JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。除非服务器部署额外的逻辑。

 

  参考链接:

  https://javaguide.cn/system-design/security/jwt-intro.html

posted @ 2024-10-04 21:48  欢乐豆123  阅读(89)  评论(0编辑  收藏  举报