JWT

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

Let’s explain some concepts further.

  • Compact

Because of their smaller size, JWTs can be sent through a URL, POST parameter, or inside an HTTP header. Additionally, the smaller size means transmission is fast.

  • Self-contained

The payload contains all the required information about the user, avoiding the need to query the database more than once.

使用场景

Authentication

This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token.

Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.

Information Exchange

JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are.

Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn’t been tampered with.

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

The header typically consists of two parts:

the type of the token, which is JWT,

and the hashing algorithm being used, such as HMAC SHA256 or RSA.

eg:

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

Payload

Payload, which contains the claims.

Claims are statements about an entity (typically, the user) and additional metadata.

There are three types of claims: registered, public, and private claims.

registered claims

These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims.

Registered Claim Names

public claims

These can be defined at will by those using JWTs.

But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.

private claims

These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.

eg:

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

Signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:

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

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

Putting all together

The output is three Base64-URL strings separated by dots that can be easily passed in HTML and HTTP environments, while being more compact when compared to XML-based standards such as SAML.

官网例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

工作机制

2018-03-35-jwt-workflow.png

  • 无状态

This is a stateless authentication mechanism as the user state is never saved in server memory.

The server’s protected routes will check for a valid JWT in the Authorization header, and if it’s present, the user will be allowed to access protected resources.

As JWTs are self-contained, all the necessary information is there, reducing the need to query the database multiple times.

  • 私有信息

Do note that with signed tokens, all the information contained within the token is exposed to users or other parties, even though they are unable to change it.

This means you should not put secret information within the token.

优势何在

  • 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