JWT

JSON Web Tokens (JWT) 是一种开放的行业标准 RFC 7519 方法,用于在两方之间安全地表示声明。

让我们进一步解释一些概念。

  • 紧凑性

由于其较小的尺寸,JWT 可以通过 URL、POST 参数或 HTTP 标头发送。此外,较小的尺寸意味着传输速度很快。

  • 自包含性

有效载荷包含关于用户的所有必要信息,避免了多次查询数据库的需要。

使用场景

认证

这是使用 JWT 最常见的场景。一旦用户登录,每个后续请求将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。

单点登录(Single Sign On)是如今广泛使用 JWT 的一个功能,因为它的开销小并且能够轻松地跨不同域使用。

信息交换

JSON Web Tokens 是在各方之间安全传输信息的一种好方法。由于 JWT 可以签名——例如,使用 公钥/私钥 对——你可以确保发送者是他们声称的那个人。

此外,由于签名是使用标头和有效载荷计算的,你还可以验证内容没有被篡改。

JWT 的优势

或者说为什么使用 jwt?

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。

传统方式

互联网服务离不开用户认证。

一般流程是下面这样。

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT 结构

各个部分之间以 . 分割。内容如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

常见的内容如下:

XXXXXX.YYYYYYYY.ZZZZZZZ

Header(头部)

头部通常包含两部分:

  • 令牌的类型,即 JWT
  • 使用的哈希算法,例如 HMAC SHA256 或 RSA

例如:

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

Payload(负载)

负载包含声明(claims)。

声明是关于一个实体(通常是用户)和附加元数据的陈述。

声明有三种类型:注册声明、公共声明和私有声明。

注册声明

这些是预定义的一组声明,不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。

注册声明名称

公共声明

这些声明可以由使用 JWT 的人随意定义。

但为了避免冲突,它们应该在 IANA JSON Web Token Registry 中定义,或者定义为包含抗冲突命名空间的 URI。

私有声明

这些是用于在同意使用它们的各方之间共享信息的自定义声明,既不是注册声明也不是公共声明。

例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature(签名)

要创建签名部分,您需要对头部进行编码,对负载进行编码,一个密钥,头部中指定的算法,并进行签名。

例如,如果您想使用 HMAC SHA256 算法,签名将按以下方式创建:

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

签名用于验证消息在传输过程中未被更改,并且在使用私钥签名的令牌的情况下,还可以验证 JWT 的发送者是否是其所声称的人。

将所有部分结合起来

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

官网例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

工作机制

JWT 工作流程

  • 无状态

这是一个无状态的认证机制,因为用户状态从未保存在服务器内存中。

服务器的受保护路由会检查 Authorization 标头中的 JWT,如果存在有效的 JWT,用户将被允许访问受保护的资源。

由于 JWT 是自包含的,所有必要的信息都在其中,减少了多次查询数据库的需求。

  • 私有信息

请注意,对于签名令牌,令牌中包含的所有信息都会暴露给用户或其他方,尽管他们无法更改这些信息。

这意味着你不应该在令牌中包含秘密信息

优势何在

  • Smaller (means faster)

  • Security-wise

简单案例

maven 引入

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

java 入门代码

package com.ryo.jdk.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>  </p>
 * <p>
 * <p> Created At 2018/3/25 15:20  </p>
 *
 * @author bbhou
 * @version 1.0
 * @since JDK 1.8
 */
public class JwtToken {

    /**
     * 密匙
     */
    private static final String SECRET = "houbinbin";

    /**
     * create token
     * @return
     */
    public static String createToken() throws UnsupportedEncodingException {
        //签发时间
        Date issuanceTime = new Date();

        //过期时间-1分钟过期
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE, 1);
        Date expireTime = nowTime.getTime();

        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");

        return JWT.create().withHeader(map)
                .withClaim("name", "houbinbin")
                .withClaim("age", "24")
                .withClaim("org", "alibaba")
                .withExpiresAt(expireTime)
                .withIssuedAt(issuanceTime)
                .sign(Algorithm.HMAC256(SECRET));
    }

    /**
     * verify token
     * @param token
     * @return
     * @throws UnsupportedEncodingException
     */
    public static Map<String, Claim> verifyToken(final String token) throws UnsupportedEncodingException {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT.getClaims();
    }

    
}
  • 测试代码
public static void main(String[] args) throws UnsupportedEncodingException {
    String token = createToken();
    System.out.println(token);

    Map<String, Claim> map = verifyToken(token);
    System.out.println(map.get("name").asString());
    System.out.println(map.get("age").asString());
    System.out.println(map.get("org").asString());

    // the expire date
    final String expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmciOiJhbGliYWJhIiwibmFtZSI6ImhvdWJpbmJpbiIsImV4cCI6MTUyMTk2MzIzNSwiaWF0IjoxNTIxOTYzMTc1LCJhZ2UiOiIyNCJ9.BZzqol65f0Z15Rdt8laxFHPDlT_jIxmVhWI81BVdIlQ";
    Map<String, Claim> map2 = verifyToken(expiredToken);
}

使用方式

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

此后,客户端每次与服务器通信,都要带上这个 JWT。

你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

生成 Token

String token = Jwts.builder().setSubject(userId)
              .setExpiration(new Date(System.currentTimeMillis() + Constant.TOKEN_EXP_TIME))
              .claim("roles", Constant.USER_TYPE_EMP)
              .setIssuedAt(new Date())
              .signWith(SignatureAlgorithm.HS256, Constant.JWT_SECRET).compact();
loginResponse.setToken(token);

验证 JWT Token

final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
   log.debug("no Authorization ", e);
   return;
} else {
   try {
       final String token = authHeader.substring(7); // The part after "Bearer "
       log.debug("token " + token);
       final Claims claims = Jwts.parser().setSigningKey(Constant.JWT_SECRET)
               .parseClaimsJws(token).getBody();
       log.debug(claims.toString());
   } catch (Exception e) { //包含超时,签名错误等异常
       log.debug("JWT Exception", e);
       return;
   }
}

特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

常见问题

JWT Token需要持久化在Memcached中吗?

不应该这样做,这样就背离了JWT通过算法验证的初心。

在退出登录时怎样实现JWT Token失效呢?

退出登录,只要客户端端把Token丢弃就可以了,服务器端不需要废弃Token。

ps: 这句话的意思时,token 是在登录时我们放在客户端(比如 web 的 cookie/storage)中的,退出时客户端把这个 token 删除即可。

Json Web Token 安全问题

Json Web Token 历险记

参考资料

jwt

jwt 入门教程

RFC 7519

  • 失效问题

登录登出时 token 的有效性问题

JWT安全验证常见疑问解答

  • 安全问题

https://www.anquanke.com/post/id/145540