JWT随笔

注:本文文字部分笔记整理自 https://baijiahao.baidu.com/s?id=1645628469934418895&wfr=spider&for=pc,作者:朱钢 | 喵叔,责编  胡巍巍

Before

JWT :Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,常用于跨域身份验证,以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。

通常情况下,Internet 服务的身份验正过程是这样的,客户端向服务器发送登录名和密码,服务器验证后将信息保存到当前会话中,这些信息包括权限、角色等。服务器向客户端返回 Session ,Session 信息会写入到客户端的 Cookie 中。后面的请求会从 Cookie 中读取 Session 发送给服务器,服务器在收到 Session 后会对比保存的数据来确认客户端身份。

但是上述模式存在一个问题,无法横向扩展。在服务器集群或者面向服务且跨域的结构中,需要数据库来保存 Session 会话,实现服务器之间的会话数据共享。

在单点登录中我们会遇到上述问题,当有多个网站提供同一拨服务,那么我们该怎么实现在甲网站登陆后其他网站也同时登录呢?

其中一种方法是持久化 Session 数据,也就是上面所说的将 Session 会话存到数据库中。这个方法的优点是架构清晰明了。但是缺点也非常明显,架构修改很困难,验证逻辑需要重写,并且整体依赖于数据库,如果存储 Session 会话的数据库挂掉那么整个身份认证就无法使用,进而导致系统无法登录。要解决这个问题我们就用到了 JWT

JWT 简述

客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。

在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。

验证不通过的情况有很多,比如签名不正确、无权限等。在 JWT 中服务器不保存任何会话数据,使得服务器更加容易扩展。

Base64URL 算法

  Base64URL算法和 Base64 算法类似,有一点区别,这个算法使用于 URL 的,因此它将 Base64 中的 + 、 / 、 = 三个字符替换成了 - 、 _ ,删除掉了 = 。因为这个三个字符在 URL 中有特殊含义

JWT 组成结构

  JWT 是由三段字符串和两个 . 组成,字符串之间没有换行(如:xxxxxx.yyyyyy.zzzzzz),每个字符串代表了不同的功能,下面将这三个字符串的功能按顺序列出来并讲解

JWT 头

  JWT 头描述了 JWT 元数据,是一个 JSON 对象,格式如:json{"alg":"HS256","typ":"JWT"}

  alg 属性表示签名所使用的算法。JWT 签名默认的算法为 HMAC SHA256 , alg 属性值 HS256 就是 HMAC SHA256 算法。

  typ 属性表示令牌类型,这里就是 JWT

有效载荷

  有效载荷是 JWT 的主体,同样也是个 JSON 对象。有效载荷包含三个部分:

  1.标准注册声明

    标准注册声明不是强制使用是的,但是建议使用。它一般包括以下内容:

    iss:jwt的签发者/发行人;

    sub:主题;

    aud:接收方;

    exp:jwt过期时间;

    nbf:jwt生效时间;

    iat:签发时间

    jti:jwt唯一身份标识,可以避免重放攻击

  2.公共声明

    可以在公共声明添加任何信息,一般会在里面添加用户信息和业务信息,但是不建议添加敏感信息,因为公共声明部分可以在客户端解密。

  3.私有声明

    私有声明是服务器和客户端共同定义的声明,同样这里不建议添加敏感信息。

 

  下面这个代码段就是定义了一个有效载荷:

  json{"exp":"201909181230","role":"admin","isShow":false}

哈希签名

  哈希签名的算法主要是确保数据不会被篡改。它主要是对前面所讲的两个部分进行签名,通过 JWT 头定义的算法生成哈希。哈希签名的过程如下:

    1. 指定密码,密码保存在服务器中,不能向客户端公开;

    2. 使用 JWT 头指定的算法进行签名,进行签名前需要对 JWT 头和有效载荷进行 Base64URL 编码,JWT 头和邮箱载荷编码后的结果之间需要用 . 来连接。

 

  简单示例如下:

  HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)

  最终结果如下:

  base64UrlEncode(JWT 头)+"."+base64UrlEncode(有效载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)

JWT 注意事项

  1. JWT 默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密;

  2. JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改;

  3. JWT 包含认证信息,如果泄露了,任何人都可以获得令牌所有的权限;因此 JWT 有效期不能太长,对于重要操作每次请求都必须进行身份验证

JWT 实现

  1. 自定义 JWT(C#)

    定义 JWT 头:csharpstring jwtHeader = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";

    定义有效载荷:csharpstringexp = GetTimeStamp(DateTime.Now.AddHours(1));string jwtHeader = "{\"name\":\"zhangsan\",\"exp\":\"" + exp + "\",\"jti\":\"123123\"}";

    加密 JWT 头和有效载荷生成哈希签名:string signature = HMACSHA256(jwtHeaderBase64Url + "." + jwtPlayloadBase64Url,"123123");

    按顺序链接三部分,最终形成 JWT:string jwtStr = jwtHeaderBase64Url + "." + jwtPlayloadBase64Url + "." + signature;

  2. 使用 .NET JWT 包

    上面的代码我们在造轮子,但是 NuGet 中已经有造好的轮子了。在 NuGet 中搜索 JWT 并安装。使用 JWT 包我们只需要自定义有效载荷和密码即可,可生成三段格式的字符串

    JWT 生成代码(略)

    同样,可以利用 JWT 包对生成的 JWT 进行解密(略)

补充

  1. 客户端发送 JWT 发送给服务器时,最好把 JWT 放在HTTP请求的Header Authorization,格式是:Authorization: Bearer jwt。

  2. JWT 不仅仅可以实现身份认证还可以在跨域 post 请求时将请求参数加入到有效载荷中,实现 post 跨域请求

附:

JAVA示例

pom依赖:

 1         <!--JWT依赖-->
 2         <dependency>
 3             <groupId>io.jsonwebtoken</groupId>
 4             <artifactId>jjwt</artifactId>
 5             <version>0.9.0</version>
 6         </dependency>
 7         <dependency>
 8             <groupId>com.auth0</groupId>
 9             <artifactId>java-jwt</artifactId>
10             <version>3.10.3</version>
11         </dependency>

代码示例:

 1 package cn.eric.other.incrypt;
 2 
 3 import com.auth0.jwt.JWT;
 4 import com.auth0.jwt.JWTVerifier;
 5 import com.auth0.jwt.algorithms.Algorithm;
 6 import com.auth0.jwt.interfaces.DecodedJWT;
 7 import org.junit.Test;
 8 
 9 import java.util.Date;
10 import java.util.HashMap;
11 
12 public class JwtDemo {
13     public static String TOKEN_SECRET = "abcdefghijklmn123654789"; // 密钥部分 这个要保密
14     public static long EXPIRE_TIME = 24 * 60 * 60 * 1000; // 过期时间 1天
15 
16     @Test
17     public void create() {
18         Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
19         String username = "eric";
20         String userId = "007";
21         try {
22             Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 算法对象
23             //设置头信息
24             HashMap<String, Object> header = new HashMap();
25             header.put("typ", "JWT"); // 加密类型是 JWT
26             header.put("alg", "HS256"); // 算法是 HS256
27             String token = JWT.create()
28                     .withHeader(header) // JWT添加header
29                     .withClaim("loginName", username) // 声明loginName
30                     .withClaim("userId", userId) // 声明userId
31                     .withExpiresAt(date) // 设置过期时间
32                     .sign(algorithm); // 签名
33             System.out.println("token:\n\t" + token);
34         } catch (Exception e) {
35             e.printStackTrace();
36         }
37     }
38 
39     @Test
40     public void verify() {
41         try {
42             Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 算法对象
43             JWTVerifier verifier = JWT.require(algorithm).build(); // JWT验证器
44             // 这个token由上面复制过来
45             DecodedJWT jwt = verifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbk5hbWUiOiJlcmljIiwiZXhwIjoxNTk2NzkwNDQwLCJ1c2VySWQiOiIwMDcifQ.BpzABKY_pATE-VrdFIIE19C5Zuhr3xApI5TN5GrwYxo");
46             System.out.println("header:\n\t" + jwt.getHeader()); // header
47             System.out.println("payload:\n\t" + jwt.getPayload()); // payload
48             System.out.println("singature:\n\t" + jwt.getSignature()); // singature
49             System.out.println("token:\n\t" + jwt.getToken()); // token
50 
51             // header中的元素用getHeaderClaim()获取
52             System.out.println("header中的typ:\n\t" + jwt.getHeaderClaim("typ").asString()); // header中的typ
53             System.out.println("header中的alg:\n\t" + jwt.getHeaderClaim("alg").asString()); // header中的alg
54 
55             // payload中的元素有2种写法
56             // getClaim("loginName")
57             // getClaims().get("loginName")
58             System.out.println("用户名:\n\t" + jwt.getClaim("loginName").asString()); // 用户名
59             System.out.println("userId:\n\t" + jwt.getClaims().get("userId").asString()); // userId
60 
61             System.out.println("失效时间(到期时间):\n\t" + jwt.getExpiresAt()); // 失效时间(到期时间)
62         } catch (Exception e) {
63             System.out.println("验证失败");
64             e.printStackTrace();
65         }
66     }
67 }

create测试结果:

 token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbk5hbWUiOiJlcmljIiwiZXhwIjoxNTk2NzkxMTY3LCJ1c2VySWQiOiIwMDcifQ.hWbl7Iem_udkSDgIauaQMmaasQgr4kHTbKY4dVy9oDw 

verify测试结果

header:
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload:
    eyJsb2dpbk5hbWUiOiJlcmljIiwiZXhwIjoxNTk2NzkwNDQwLCJ1c2VySWQiOiIwMDcifQ
singature:
    BpzABKY_pATE-VrdFIIE19C5Zuhr3xApI5TN5GrwYxo
token:
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbk5hbWUiOiJlcmljIiwiZXhwIjoxNTk2NzkwNDQwLCJ1c2VySWQiOiIwMDcifQ.BpzABKY_pATE-VrdFIIE19C5Zuhr3xApI5TN5GrwYxo
header中的typ:
    JWT
header中的alg:
    HS256
用户名:
    eric
userId:
    007
失效时间(到期时间):
    Fri Aug 07 16:54:00 CST 2020

 

自定义JAVA工具类

  1 package com.cloud.XXX.base.utils;
  2 
  3 import com.auth0.jwt.JWT;
  4 import com.auth0.jwt.JWTVerifier;
  5 import com.auth0.jwt.algorithms.Algorithm;
  6 import com.auth0.jwt.exceptions.JWTDecodeException;
  7 import com.auth0.jwt.interfaces.DecodedJWT;
  8 import com.cloud.gp.base.constant.CommonConst;
  9 import lombok.extern.slf4j.Slf4j;
 10 
 11 import javax.servlet.ServletRequest;
 12 import javax.servlet.http.HttpServletRequest;
 13 import java.io.UnsupportedEncodingException;
 14 import java.util.Date;
 15 
 16 @Slf4j
 17 public class JWTUtil {
 18     /**过期时间**/
 19     public static final long EXPIRE_TIME = 60*60*24*30;
 20     /**秘钥**/
 21     public static final String SECRET = "XXX_JWT";
 22     /**用户字段**/
 23     public static final String ACCOUNT = "username";
 24     /**用户字段**/
 25     public static final String ACCOUNT_ID = "userId";
 26     /**令牌与权限前缀**/
 27     public static final String TOKEN_PERMS_PREFIX = "TOKEN_PERMS:";
 28     /**令牌与username前缀**/
 29     public static final String TOKEN_USERNAME_PREFIX = "TOKEN_USERNAME:";
 30 
 31     /**
 32      * 生成 token并设置过期时间
 33      * @param account 用户名
 34      * @param userId 用户ID(提高性能用)
 35      * @return 加密的token
 36      */
 37     public static String createToken(String account,String userId) {
 38         try {
 39             //动态秘钥
 40             String secret = account + SECRET;
 41             Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME*1000);
 42             //签名方式
 43             Algorithm algorithm = Algorithm.HMAC256(secret);
 44             //生成Token(包含用户名,时间戳/保留)
 45             return JWT.create()
 46                     .withClaim(ACCOUNT, account)
 47                     .withClaim(ACCOUNT_ID, userId)
 48                     .withExpiresAt(date)
 49                     .sign(algorithm);
 50         } catch (UnsupportedEncodingException e) {
 51             return null;
 52         }
 53     }
 54 
 55     /**
 56      * 校验 token 是否正确
 57      * @param token  密钥
 58      * @return 是否正确
 59      */
 60     public static boolean verify(String token) {
 61         try {
 62             String secret = getClaim(token,ACCOUNT) + SECRET;
 63             Algorithm algorithm = Algorithm.HMAC256(secret);
 64             JWTVerifier verifier = JWT.require(algorithm).build();
 65             verifier.verify(token);
 66         }catch (Exception e){
 67             return false;
 68         }
 69         return true;
 70     }
 71 
 72     /**
 73      * 获得token中的信息,无需secret解密也能获得
 74      * @param token  令牌
 75      * @param clainm 需要获取的信息
 76      * @return token中包含的用户名
 77      */
 78     public static String getClaim(String token,String clainm) {
 79         try {
 80             DecodedJWT jwt = JWT.decode(token);
 81             return jwt.getClaim(clainm).asString();
 82         } catch (JWTDecodeException e) {
 83             return null;
 84         }
 85     }
 86 
 87     /**
 88      * 获取请求对象中的token值
 89      * @param request 请求对象
 90      * @return token
 91      */
 92     public static String getToken(ServletRequest request){
 93         HttpServletRequest req = (HttpServletRequest) request;
 94         String token = req.getHeader(CommonConst.REQUEST_TOKEN);
 95         if(token==null){
 96             token = req.getParameter(CommonConst.REQUEST_TOKEN);
 97         }
 98         return token;
 99     }
100 
101     /**
102      * 获取请求对象中的token值中的信息
103      * @param request 请求对象
104      * @param claim 需要获取的信息key
105      * @return106      */
107     public static String getTokenDetails(ServletRequest request, String claim){
108         String token = getToken(request);
109         return getClaim(token,claim);
110     }
111 }

写main方法运行测试

 1     public static void main(String[] args) {
 2         // 生成token
 3         String username = "13012345678";
 4         String userId = "10010";
 5         String token = createToken(username, userId);
 6         System.out.println("token:" + token);
 7 
 8         // 获取用户信息
 9         token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTkyOTkyODksInVzZXJJZCI6IjEwMDEwIiwidXNlcm5hbWUiOiIxMzAxMjM0NTY3OCJ9.AzKcOLKANLaT9kpax8gNfD5hwW8CJvx8EHpLEUZDHfs";
10         username = getClaim(token, "username");
11         System.out.println("根据token获取想要得到的字段'username'的信息" + username);
12 
13         // 验证登录
14         boolean b = verify(token);
15         System.out.println("登录是否成功:" + b);
16     }

测试结果:

 token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTkyOTkyODksInVzZXJJZCI6IjEwMDEwIiwidXNlcm5hbWUiOiIxMzAxMjM0NTY3OCJ9.AzKcOLKANLaT9kpax8gNfD5hwW8CJvx8EHpLEUZDHfs 根据token获取想要得到的字段'username'的信息13012345678 登录是否成功:true 

posted @ 2020-08-06 15:31  先天下  阅读(180)  评论(0)    收藏  举报