zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Java岗 面试考点精讲(基础篇02期)
    面试必会之HashMap源码分析
    Java岗 面试考点精讲(基础篇01期)
    面试被怼?刷个题吧
    Centos7和win7双系统调整默认启动
    SSL证书在线工具
    read,for,case,while,if简单例子
    从微软下载安装Windows10
    grep,sed,cut,awk,join个性特点
    iMac一体机安装苹果和Win7双系统
  • 原文地址:https://www.cnblogs.com/miaoying/p/10885258.html
Copyright © 2011-2022 走看看