前言

JWT官网
JSON Web Token (JWT) 是一个通过JSON形式作为WEB应用的令牌,用于在各方之间以 JSON 对象的形式安全传输信息。在传输过程中可以完成数据加密、签名等操作。
以下是JWT使用的一些场景:

  • 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
  • 信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以您可以确定发件人就是他们所说的那个人。此外,由于使用标头和有效负载计算签名,您还可以验证内容没有被篡改。
  • 单点登录:“一次登录,处处登录”,所有应用系统共享一个身份认证系统,如我们在百度旗下的产品:“百度搜索” 的网页进行登录了,那么当我们访问百度旗下的其它产品网址,如 “百度网盘” 就无需再次登录,利用JWT就可以很好的实现此功能

    原理

    传统的Seesion认证

    客户端发送登录请求,服务器端认证通过后将用户信息保存在session中,然后返回给客户端sessionID(JSESSIONID),客户端将sessionID用cookie保存起来,下次用户登陆时会携带cookie,通过cookie中的sessionID(JSESSIONID)获取服务器端session中的用户信息。

    cookie是储存在客户端的,session是储存在服务器端的,由于cookie储存在本地,所以更容易被破解

JWT - 图1
缺点:
可以看到传统的session登录,每次用户认证登陆时,都会在服务器端进行记录保存,而通常session都是储存在内层中的,当登录用户过多时,无疑会增加服务器消耗。
由于用户在做认证后,用户认证信息被保存在内存中,意味着下次用户再次请求时,仍然需要请求服务器,才能拿到授权资源,这样的处理方式在分布式应用中,相应的限制了负载均衡等一些扩展操作的能力。
由于session是基于cookie进行用户识别的,如果cookie被截获,用户就会很容易受到跨站请求伪造的攻击,并且由于sessionID只是一个特征值,表达的信息不够丰富,也大大限制了扩展操作。

JWT认证

image.png
客户端将用户名及密码发送给服务器端做校验,服务器端校验通过后,将用户ID及其它信息作为JWT的负载(PayLoad),将其与头部(Header)分别进行base64编码拼接后签名(Signature),形成一个JWT【所以JWT中是包含了用户信息的(即自包含),也就不需要向传统的Session认证一样,去服务器端请求用户信息】,并返回给客户端进行本地保存(cookie或者localStorage)。
客户端在每次请求时将JWT放入HTTP Header中的Authorization位(用以解决XSS和XSRF问题)。服务器端检查是否存在,若存在则验证JWT的有效性(检查签名是否正确,Token是否过期,Token身份信息等),验证通过后,服务器端执行相应的操作,并返回给客户端。

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

  1. {
  2. "姓名": "张三",
  3. "角色": "管理员",
  4. "到期时间": "2022年2月2日2点2分"
  5. }

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

结构

JSON Web Tokens 由以点(.)分隔的三部分组成,它们是:

  1. 标头(Header)
  2. 负载(Payload)
  3. 签名(Signature)

因此,JWT 通常是这样的:xxxxx.yyyyy.zzzzz

Header

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

  1. {
  2. "alg": "HS256",
  3. "typ": "JWT"
  4. }

然后,这个 JSON 被Base64Url编码以形成 JWT 的第一部分。

Payload

令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的陈述。
示例:

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

然后对有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。

请注意,对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。

Signation

要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密、标头中指定的算法,并对其进行签名。

例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建(即:对 base64编码的标头+base64编码的负载+盐 进行加密签名)

  1. HMACSHA256(
  2. base64UrlEncode(header) + "." +
  3. base64UrlEncode(payload),
  4. secret)

最后一步的签名其实就是对头部和负载内容进行签名,防止内容信息被篡改。并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者就是它所说的那个人。
JWT 规定了7个官方字段,供选用:

  1. iss (issuer):签发人
  2. exp (expiration time):过期时间
  3. sub (subject):主题
  4. aud (audience):受众
  5. nbf (Not Before):生效时间
  6. iat (Issued At):签发时间
  7. jti (JWT ID):编号

总结

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

下面显示了一个 JWT,该 JWT 具有先前的标头和有效负载编码,并使用秘密签名(下面只是为了演示效果,实际是没有换行的)
JWT - 图3
可以使用jwt.io Debugger来解码、验证和生成 JWT。
image.png

总结

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

    整合SpringBoot

    简单使用

    创建一个SpringBoot项目
    引入JWT:
    1. <dependency>
    2. <groupId>com.auth0</groupId>
    3. <artifactId>java-jwt</artifactId>
    4. <version>3.18.3</version>
    5. </dependency>
    ```java import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import org.junit.Test;

import java.util.Calendar; import java.util.HashMap;

public class JwtApplicationTests {

  1. /**
  2. * 生成令牌
  3. */
  4. @Test
  5. public void contextLoads() {
  6. HashMap<String, Object> map = new HashMap<>();
  7. Calendar instance = Calendar.getInstance();
  8. // 生成过期时间为600秒后
  9. instance.add(Calendar.SECOND,600);
  10. // 生成过期时间为1天后
  11. // instance.add(Calendar.DATE,1);
  12. String token = JWT.create()
  13. // 标头
  14. .withHeader(map)
  15. // 负载
  16. .withClaim("userId", 1001)
  17. .withClaim("userName", "ahzoo")
  18. // 过期时间
  19. .withExpiresAt(instance.getTime())
  20. // 签名
  21. .sign(Algorithm.HMAC256("999"));
  22. System.out.println(token);//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9.yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8
  23. }
  24. /**
  25. * 验证令牌
  26. */
  27. @Test
  28. public void verify(){
  29. // 创建验证对象
  30. JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("999")).build();
  31. String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9.yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8";
  32. // 验签,验签失败会抛异常
  33. DecodedJWT verify = jwtVerifier.verify(token);
  34. System.out.println(verify.getHeader());
  35. System.out.println(verify.getPayload());
  36. System.out.println(verify.getSignature());
  37. System.out.println(verify.getClaims());
  38. // 默认只能拿String类型数据,获取其它类型数据时需要调用额外的方法
  39. System.out.println(verify.getClaim("userId").asInt());
  40. // 获取String类型数据,就不用调用额外的方法
  41. System.out.println(verify.getClaim("userName"));
  42. System.out.println(verify.getExpiresAt());

/ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9 yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8 {userName=”ahzoo”, exp=1643526751, userId=1001} 1001 “ahzoo” Sun Jan 30 15:12:31 CST 2022 /

  1. }

}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22390397/1643526216670-dbc76499-a48d-4f35-bbee-76c63dc0f0a3.png#clientId=u57e0b00d-144e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=732&id=u1bd78b40&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1007&originWidth=1906&originalType=binary&ratio=1&rotation=0&showTitle=false&size=264776&status=done&style=none&taskId=u13166d42-891b-42f7-87d4-6d08e6f1f5d&title=&width=1386.1818181818182)<br />封装JWT工具类:
  2. ```java
  3. import com.auth0.jwt.JWT;
  4. import com.auth0.jwt.JWTCreator;
  5. import com.auth0.jwt.algorithms.Algorithm;
  6. import com.auth0.jwt.interfaces.Claim;
  7. import com.auth0.jwt.interfaces.DecodedJWT;
  8. import java.util.Calendar;
  9. import java.util.Map;
  10. public class JWTUtil {
  11. /**
  12. * 密钥
  13. */
  14. private static String SECRET = "privatekeyahzoo";
  15. /**
  16. * 传入payload信息获取token
  17. * @param map payload
  18. * @return token
  19. */
  20. public static String getToken(Map<String, String> map) {
  21. JWTCreator.Builder builder = JWT.create();
  22. //payload
  23. map.forEach(builder::withClaim);
  24. Calendar instance = Calendar.getInstance();
  25. // 默认7天过期
  26. instance.add(Calendar.DATE, 7);
  27. // 指定令牌的过期时间
  28. builder.withExpiresAt(instance.getTime());
  29. return builder.sign(Algorithm.HMAC256(SECRET));
  30. }
  31. /**
  32. * 验证token
  33. */
  34. public static DecodedJWT verify(String token) {
  35. // 验证失败会抛异常
  36. return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
  37. }
  38. /**
  39. * 获取token中的payload
  40. */
  41. public static Map<String, Claim> getPayloadFromToken(String token) {
  42. return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token).getClaims();
  43. }
  44. }