漫谈JSON Web Token(JWT)

一、背景

传统的单体应用基于cookie-session的身份验证流程一般是这样的:

  1. 用户向服务器发送账户和密码。
  2. 服务器验证账号密码成功后,相关数据(用户角色、登录时间等)都保存到当前会话中。
  3. 服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中。
  4. 以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid作为索引获取到具体session。

但是这种模式存在如下几个问题:

  1. 没有分布式架构无法支持横向扩展,例如站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站,满足不了这种需求。
  2. 如果用户很多,这些信息存储在服务器内存中会给服务器增加负担。
  3. 还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

针对问题1,有两种解决方案,第一种就是使用session共享,比如使用redis实现,第二种解决方案就是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

二、什么是JWT

JSON WEB TOKEN(以下称JWT),JWT也是一种token,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。

2.1JWT的特点

简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
自包含(Self-contained): 负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。

2.2JWT的消息结构

实际的JWT数据结构就像这样。

 

enter description here
enter description here

它是一个很长的字符串,中间用点分隔成三个部分,红色是header,紫色部分是Payload,蓝色部分是Signature。即格式都是Header.Payload.Signature

 

Header(头部)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

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

alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。

Payload(负载)

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

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

除了官方字段我们还可以自定义字段,比如:

{
  "userId": 1234567890,
  "name": "John Doe"
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。然后这个JSON对象也要使用Base64URL算法转成字符串。

Signature(签名)

Signature是对前两个部分的签名,作用是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Base64URL算法

Base64URL算法与Base64算法有一些区别,作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_" 替换,这就是Base64URL算法。

三、JWT的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信都要带上JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

还可以将JWT放在POST请求的数据体中,或者跟在URL后面。
所以使用JWT实现单点登录,也可以放在Cookie中或者Header中,基于Cookie的实现可以参考这篇文章《八幅漫画理解使用 JWT设计的单点登录系统》,之前公司的项目是基于Header的Authorization字段字段实现的。

四、JWT的缺点

当然,JWT并不是完美的,它也有一些缺点。
1.JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
2.JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。所以为了避免盗用,可以将有效期设置的短一些,使用HTTPS协议加密传输。
理解JWT的使用场景和优劣》这篇文章介绍的很详细,但是我不太认同他说的使用jwt做单点登录+会话管理没有传统的cookie-session 机制工作得更好。

五、代码示例

JWT一般是在网关服务配合Filter来实现认证的,考虑再弄个Zuul网关比较麻烦这里就简化了下。

JwtTestController

/**
 * Created by 2YSP on 2019/8/26.
 */
@Slf4j
@RestController
@RequestMapping("/jwt")
public class JwtTestController {

  @GetMapping("/login")
  public String login(@RequestParam("username") String username,
      @RequestParam("password") String password, HttpServletResponse response) throws Exception {
    if (!checkUserNameAndPwd(username, password)) {
      return "login error:invalid username or password";
    }
    // 过期时间
    Long exp = System.currentTimeMillis() + 20 * 1000;
    PayLoad payLoad = new PayLoad(1L, username, exp);
    String token = JwtUtils.generateToken(payLoad);
    Cookie cookie = new Cookie("token", token);
    // HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击
    cookie.setHttpOnly(true);
    // 30秒
    cookie.setMaxAge(30);
    response.addCookie(cookie);
    return token;
  }

  @GetMapping("/verify")
  public Boolean verifyToken(HttpServletRequest request) {
    String token = getTokenFromCookie(request);
    if (StringUtils.isBlank(token)){
      return false;
    }
    // 验证签名
    if (!JwtUtils.checkSignature(token)){
      return false;
    }
    PayLoad payLoad = JwtUtils.getPayLoad(token);
    if (payLoad.getExp() < System.currentTimeMillis()){
      // 已过期
      return false;
    }
    Gson gson = new Gson();
    log.info("verify successfully ,payLoad:{} ", gson.toJson(payLoad));
    return true;
  }

  private String getTokenFromCookie(HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    for(Cookie cookie : cookies){
      if (cookie.getName().equals("token")){
          return cookie.getValue();
      }
    }
    return null;
  }

  private boolean checkUserNameAndPwd(String username, String pwd) {
    if (StringUtils.isBlank(username)) {
      return false;
    }
    if (StringUtils.isBlank(pwd)) {
      return false;
    }
    if (username.equals("admin") && pwd.equals("1234")) {
      return true;
    }
    return false;
  }
}

这里提供了两个接口,一个是模拟登录(登录一般是POST的这里方便测试就改为GET方式了),登录成功后返回一个token同时将token保存在cookie中。另一个是校验token的接口,依次进行验签、过期时间等校验。
PayLoad

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayLoad {

  private Long userId;

  private String name;

  private Long exp;

}

保存一些用户信息和过期时间的实体类。
JwtUtils

  1. public class JwtUtils
  2.  
  3. /** 
  4. * 加密算法 
  5. */ 
  6. public static final String HEADER_ALG = "HS256"
  7.  
  8. public static final String HEADER_TYP = "JWT"
  9. /** 
  10. * 加密串 
  11. */ 
  12. public static final String SECRET = "d5ec0a02"
  13.  
  14. public static String generateToken(PayLoad payLoad) throws Exception
  15. Gson gson = new Gson(); 
  16. Header header = new Header(HEADER_ALG, HEADER_TYP); 
  17. String encodedHeader = Base64Utils.encodeToUrlSafeString(gson.toJson(header).getBytes( 
  18. Charsets.UTF_8)); 
  19. String encodePayLoad = Base64Utils.encodeToUrlSafeString(gson.toJson(payLoad).getBytes( 
  20. Charsets.UTF_8)); 
  21. String signature = HMACSHA256(encodedHeader + "." + encodePayLoad, SECRET); 
  22. return encodedHeader + "." + encodePayLoad + "." + signature; 
  23.  
  24. public static boolean checkSignature(String token)
  25. String[] array = token.split("\\."); 
  26. if (array.length != 3) { 
  27. throw new IllegalArgumentException("token error"); 
  28. try
  29. String signature = HMACSHA256(array[0] + "." + array[1], SECRET); 
  30. return signature.equals(array[2]); 
  31. } catch (Exception e) { 
  32. return false
  33.  
  34. public static PayLoad getPayLoad(String token)
  35. String[] array = token.split("\\."); 
  36. if (array.length != 3) { 
  37. throw new IllegalArgumentException("token error"); 
  38. String payLoad = new String(Base64Utils.decodeFromUrlSafeString(array[1]), Charsets.UTF_8); 
  39. Gson gson = new Gson(); 
  40. return gson.fromJson(payLoad, PayLoad.class); 
  41.  
  42.  
  43. public static String HMACSHA256(String data, String key) throws Exception
  44.  
  45. Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 
  46.  
  47. SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 
  48.  
  49. sha256_HMAC.init(secret_key); 
  50.  
  51. byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); 
  52.  
  53. StringBuilder sb = new StringBuilder(); 
  54.  
  55. for (byte item : array) { 
  56.  
  57. sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 
  58.  
  59.  
  60. return sb.toString().toUpperCase(); 
  61.  
  62.  
  63. @Data 
  64. @AllArgsConstructor 
  65. static class Header
  66.  
  67. private String alg; 
  68. private String typ; 
  69.  
  70.  

JWT的工具类,提供了一些生成token,验证签名等方法。代码地址

posted @ 2019-08-26 16:48  烟味i  阅读(332)  评论(1编辑  收藏  举报