• 漫谈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,验证签名等方法。代码地址

  • 相关阅读:
    Daily Scrum 11.20
    Daily Scrum 11.19
    Daily Scrum 11.18
    Daily Scrum 11.17
    Daily Scrum 11.16
    Daily Scrum 11.15
    Loj10222佳佳的 Fibonacci
    CH3801Rainbow的信号
    js仓库。。。
    【UVa1635】Irrelevant Elements
  • 原文地址:https://www.cnblogs.com/2YSP/p/11413478.html
Copyright © 2020-2023  润新知