JWT了解与实战

最近在使用JWT做一个单点登录与接口鉴权的功能,正好可以对JWT有深一步的了解。

一、JWT使用场景:

1. 授权:用户登录后,每个请求都包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT地一个特性,因为它开销小,并且可以轻松地跨域使用。

2. 信息交换: 对于安全的在各方之间传输信息而言,JWT无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,还可以验证内容有没有被篡改。

使用依赖:

        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>7.1</version>
        </dependency>

核心代码:

    /**
     * 采用HS256算法生成token
     *
     * @param payLoadMap 荷载
     * @return
     * @throws JOSEException
     */
    public static String createTokenHS256(Map<String, Object> payLoadMap) throws JOSEException {
        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
        Payload payload = new Payload(new JSONObject(payLoadMap));
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        log.info("header : " + jwsHeader);
        log.info("payload : " + payload);
        JWSSigner jwsSigner = new MACSigner(TokenConstants.SECRET);
        jwsObject.sign(jwsSigner);
        return jwsObject.serialize();
    }

    /**
     * 解析token
     *
     * @param token
     * @return
     * @throws ParseException
     * @throws JOSEException
     */
    public static Map<String, Object> parseTokenHS256(String token) throws ParseException, JOSEException {
        JWSObject jwsObject = JWSObject.parse(token);
        JWSVerifier jwsVerifier = new MACVerifier(TokenConstants.SECRET);
        return verify(jwsObject, jwsVerifier);
    }

    /**
     * 验证token
     *
     * @param jwsObject
     * @param jwsVerifier
     * @return
     * @throws JOSEException
     */
    private static Map<String, Object> verify(JWSObject jwsObject, JWSVerifier jwsVerifier) throws JOSEException {
        Map<String, Object> resultMap = new HashMap<>();
        Payload payload = jwsObject.getPayload();
        if (jwsObject.verify(jwsVerifier)) {
            resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_SUCCESS);
            JSONObject jsonObject = payload.toJSONObject();
            resultMap.put(TokenConstants.DATA, jsonObject);
            if (jsonObject.containsKey(TokenConstants.EXPIRE_TIME)) {
                Long expireTime = Long.valueOf(jsonObject.get(TokenConstants.EXPIRE_TIME).toString());
                Long nowTime = System.currentTimeMillis();
                log.info("nowTime : " + nowTime);
                if (nowTime > expireTime) {
                    resultMap.clear();
                    resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_EXPIRED);
                }
            }
        } else {
            resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED);
        }
        return resultMap;
    }

单元测试:

    @Test
    public void createTokenTest() {
        String appId = "appId";
        String appSecret = "appSecret";
        Map<String, Object> map = new HashMap<>();
        map.put("appId", appId);
        map.put("appSecret", appSecret);
        Long createTime = System.currentTimeMillis();
        map.put("createTime", createTime);
        map.put("expireTime", createTime + 60 * 60 * 2);
        try {
            String token = TokenHS256Util.createTokenHS256(map);
            System.out.println(token);
            log.info("token : " + token);
        } catch (JOSEException e) {
            log.error("create token failed. ");
            e.printStackTrace();
        }
    }

    @Test
    public void ValidTokenTest() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJhcHBTZWNyZXQiOiJhcHBTZWNyZXQiLCJleHBpcmVUaW1lIjoxNTU3NDczMDI2OTg3LCJjcmVhdGVUaW1lIjoxNTU3NDczMDE5Nzg3LCJhcHBJZCI6ImFwcElkIn0.mNwTFBLOtc3hD90SI7gKV1YlahulOOartZFaLFbqK0Q";
        if (token != null) {
            try {
                Map<String, Object> validMap = TokenHS256Util.parseTokenHS256(token);
                Integer result = (Integer) validMap.getOrDefault(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED);
                switch (result) {
                    case TokenConstants.TOKEN_PARSE_SUCCESS:
                        log.info("token parse success.");
                        JSONObject jsonObject = (JSONObject) validMap.getOrDefault(TokenConstants.DATA, StringUtils.EMPTY);
                        log.info("appId = " + jsonObject.getOrDefault("appId", StringUtils.EMPTY));
                        log.info("appSecret = " + jsonObject.getOrDefault("appSecret", StringUtils.EMPTY));
                        log.info("sta = " + jsonObject.getOrDefault("sta", StringUtils.EMPTY));
                        log.info("expire = " + jsonObject.getOrDefault("expire", StringUtils.EMPTY));
                        break;
                    case TokenConstants.TOKEN_EXPIRED:
                        log.error("token has expired. ");
                        break;
                    default:
                        log.error("token parse failed. ");
                        break;
                }
            } catch (ParseException | JOSEException e) {
                e.printStackTrace();
            }
        }
    }

二、JWT组成

JWT由三部分组成,它们之间用圆点(.)连接

我们项目里的header内容固定是:

{"alg":"HS256"}

表示使用的是HS256算法,然后用base64多对这个json编码就得到JWT的第一部分,即Header。

payload包含声明(要求),声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:registered,public,private

registered claims: 这里有一组预定义的声明,它们不是强制的,但是推荐。比如iss(issure), exp(expiration time), sub(subject), aud(audience)等

public claims: 可以随意定义

private claims: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。

例如:

{"appSecret":"appSecret","expireTime":1558059574173,"createTime":1558059566973,"appId":"appId"}

对payload进行base64编码就得到JWT的第二部分

注意:不要在JWT的payload或header中放置敏感信息,除非它们是加密的。

Signature:为了得到签名部分,你必须有编码过的header、编码过的payload、一个密钥,签名算法是header中指定的那个,然后对他们签名即可。

签名是用于验证消息在传递过程中有没有被更改,并且对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

三、官网的debugger (https://jwt.io/

(每次修改verify signature里面的内容,生成的token是会改动的,所以可以验证消息在传递过程中是否有被修改)

四、JWT的工作原理

在认证时,当用户用他们的凭证成功登陆以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,需要非常小心以防出现安全问题。一般而言,保存令牌时不应该超过你所需要它的时间。

无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。

header 应该看起来是这样的:

Authorization:Bearer <token>

服务器上的受保护的路由将会检查Authorization 

header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。

如果JWT包含足够多的必须的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。

如果token是在授权头(Authorization header)中发送的,那么跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。

第一步:请求授权接口,获取授权码token
第二步:使用授权码token访问受保护的资源(比如:API)

五、基于服务器端session的身份认证

HTTP协议是无状态的,即如果我们已经认证了一个用户,那么下一次请求时,服务器不知道来者何人,需要再次进行认证。

传统的做法是将已经认证过的用户信息存储在服务器上,比如session,用户下次请求时带着session ID,然后服务器以此检查用户是否认证过。

缺陷:

1. session: 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器在此处的开销就越大。 2. scalability: 由于session是在内存中,扩展不灵活。 3. CORS: 当想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用Ajax调用从另一个域名下获取资源时,可能会遇到禁止请求的问题。 4. CSRF: 用户很容易受到CSRF攻击。

六、基于token的身份认证

基于token的身份认证是无状态的,服务器或者session中不会存储任何用户信息,没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

主要流程:
1. 用户携带用户名、密码请求访问 2. 服务器校验用户凭证 3. 应用提供一个token给客户端 4. 客户端存储token,并且在随后的每一次请求中都带着它 5. 服务器校验token并返回数据

注意:每一次请求都要token;token应该放在请求header中,我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin:*

使用token的优势:

1. 无状态和可扩展性
2. 安全,token不是cookie,每次请求的时候token都会被发送,而且由于没有cookie被发送,还有助于防止CSRF攻击。
即使在你的实现中将token存储在客户端的cookie中,这个cookie也只是一种存储机制,而非身份认证机制,没有基于会话的操作。
token在一段时间以后会过期,这个时候用户需要重新登录,这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。

内容参考:https://www.cnblogs.com/cjsblog/p/9277677.html

原文地址:https://www.cnblogs.com/miaoying/p/10885258.html