0x01 cookie/session

说到token就必然绕不开cookie和session。

cookie:由服务端给客户端颁发的一张通行证,用来验证客户端的身份,本质上是一段在浏览器上以KV形式存储的文本数据,包含了session相关信息。用于解决HTTP协议无状态的问题,所以cookie是一个会话跟踪机制,是有状态的。

session:当客户端请求服务端通过验证后,服务端会生成保存身份认证相关的session数据,并将session相关信息写入cookie返回给客户端,然后客户端将cookie保存到本地。之后两端就通过核对session信息来确认可信状态。session 可能会存储在内存、磁盘、数据库里,可能需要在服务端定期的去清理过期的 session。

0x02 token

既然有了cookie/session为啥还需要token呢

优点

1、无状态、可扩展

2、安全性

3、可扩展性

4、多平台跨域/单点登陆

5、基于标准

6、缓解服务器内存压力/增大服务器计算压力

格式

UID + TIME + SIGN [+ OTHER]

0x03 实施

JSON Web Tokens(JWT)

组成

  • header

用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等

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

base64一下

ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9

  • payload

标准文档:https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1

可以在其中添加这些字段

iss:Issuer,发行者
sub:Subject,主题
aud:Audience,观众
exp:Expiration time,过期时间
nbf:Not before
iat:Issued at,发行时间
jti:JWT ID
Map<String , Object> payload=new HashMap<String, Object>();
Date date=new Date();
payload.put("uid", "007");
payload.put("iat", date.getTime());
payload.put("ext",date.getTime()+1000*60*60);

上边代码中添加的字段如下

{
    "iat": 当前时间,
    "exp": 过期时间,
    "uid": "007"
}

base64编码

ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==

这样payload就生成好了

  • signature

将header和payload生成的base64编码通过.连接起来,如下

ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9.ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==

然后再定义一个secret,如下

secret

通过header中定义的HS256算法以secret为密钥进行加密得到signature

81faa5ef7b7596783cb3ed2f75618def367a9b7f8490047cb12880d895b794eb

此时JWT就生成了,base64(header).base64(payload) .signature

像这样ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9.ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==.81faa5ef7b7596783cb3ed2f75618def367a9b7f8490047cb12880d895b794eb

当然这种方式不能在token中携带敏感信息,例如密码

0x04 应用

  • 单点登陆

Set-Cookie: jwt=yyy.zzz.xxx; HttpOnly; max-age=980000; domain=.taobao.com

  • API 调用/授权

https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=xxxx

  • 支付验证(一次性)

  • 串行服务调用

    一次性有效,再次生成token时以用户账户和第一次token为key,update该记录来判断

  • 敏感接口多次调用

0x05 代码

生成代码

private static final JWSHeader header=new JWSHeader(JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null);
    
    /**
     * 生成token,该方法只在用户登录成功后调用
     * 
     * @param Map集合,可以存储用户id,token生成时间,token过期时间等自定义字段
     * @return token字符串,若失败则返回null
     */
    public static String createToken(Map<String, Object> payload) {
        String tokenString=null;
        // 创建一个 JWS object
        JWSObject jwsObject = new JWSObject(header, new Payload(new JSONObject(payload)));
        try {
            // 将jwsObject 进行HMAC签名
            jwsObject.sign(new MACSigner(SECRET));
            tokenString=jwsObject.serialize();
        } catch (JOSEException e) {
            System.err.println("签名失败:" + e.getMessage());
            e.printStackTrace();
        }
        return tokenString;
    }

校验代码

public static Map<String, Object> validToken(String token) {
        Map<String, Object> resultMap = new HashMap<String, Object>();
        try {
            JWSObject jwsObject = JWSObject.parse(token);
            Payload payload = jwsObject.getPayload();
            JWSVerifier verifier = new MACVerifier(SECRET);
            if (jwsObject.verify(verifier)) {
                JSONObject jsonOBj = payload.toJSONObject();
                // token校验成功(此时没有校验是否过期)
                resultMap.put("state", TokenState.VALID.toString());
                // 若payload包含ext字段,则校验是否过期
                if (jsonOBj.containsKey("ext")) {
                    long extTime = Long.valueOf(jsonOBj.get("ext").toString());
                    long curTime = new Date().getTime();
                    // 过期了
                    if (curTime > extTime) {
                        resultMap.clear();
                        resultMap.put("state", TokenState.EXPIRED.toString());
                    }
                }
                resultMap.put("data", jsonOBj);
            } else {
                // 校验失败
                resultMap.put("state", TokenState.INVALID.toString());
            }
        } catch (Exception e) {
            //e.printStackTrace();
            // token格式不合法导致的异常
            resultMap.clear();
            resultMap.put("state", TokenState.INVALID.toString());
        }
        return resultMap;
    }   

参考:
http://blog.leapoahead.com/2015/09/06/understanding-jwt/
https://github.com/bigmeow/JWT