jwt信息校验技术介绍

背景

jwt全称是JSON Web Token,用来做数据的校验,通过一个具体的json结构,作为信息的载体,定义标准(RFC7519),用来验证数据的准确和完整性。其所携带的信息,一般比较少,主要用来做身份认证。目前接触到的有在AppleId登录中使用。

说明

jwt由三段构造,如下是一个经过封装的jwt base64字串,通过.分隔。第一部分是头部header,第二部分是内容payload,最后一部分是签名signature
格式如:

eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmNoYW5nZGFvLnR0c2Nob29sIiwiZXhwIjoxNTg5Mjg2ODcyLCJpYXQiOjE1ODkyODYyNzIsInN1YiI6IjAwMTk0MC43YTExNDFhYTAwMWM0NjllYTE1NjNjNmJhZTk5YzM3ZC4wMzA3IiwiY19oYXNoIjoiX0ZUSlh3cGhpRFhHeEhIQ1VMODdVZyIsImVtYWlsIjoiYXEzMmsydnpjd0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU4OTI4NjI3Miwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.c2h4MoEjGWRmJAAppbvuJToTGf8wM510yQYgonZXIwan66KTAmYLE11NpA83xq0pvtuex-hbRjna4WeJWD1QfZsHlZ7iIhL95YoG9y8DXIhTyrd51ADLxn4gEyxOKM03aZ4M54NyoZ13V3osd-T-1tvCX86JZnDlrWkixsUONiXLXB9-G63QO5JwsQPJuorweT9-qj6NIiZmX_ayDhRFpe0FxW41u-c3LhN4dRTb1FMF2LYDymBYsdLNyAv7glDzq13M6rBeQRMRJz7e5C6PcppHhhxgrbJTcdCszB5bjA-Ck8PbnsM2qSxVW6hHd4xEpClEzUMFee8dZIi1PSU0KA

1.头部header
头部主要标识具体的签名算法。而像苹果登录会添加签名的公钥id,如下:

        {
            "kty": "RSA",
            "kid": "86D88Kf",
            "alg": "RS256"
        }

然后将这个内容使用base64编码,就构成了jwt的第一部分。
2.载荷payload
这部分主要是业务相关的核心信息,标准中定义了对应的字段,但不强制使用,如下是苹果登录的一个例子:

      {
      "iss": "DEF123GHIJ",
      "iat": 1600853455,
      "exp": 1606123855,
      "aud": "https://appleid.apple.com",
      "sub": "com.mytest.app"
      }

可以看到其中的字段名称都是三个字母,这也是标准的要求,尽量简洁。

字段 释义 说明
iss issuer 签发者
iat issue at 签发时间,时间戳秒单位
exp expire 过期时间,时间戳秒单位
aud audience 接收方
sub subject 面向的用户

如上的就是iss为DEF123GHIJ的签发者面向com.mytest.app的用户,向apple的签名。在添加后再将载荷部分用base64编码,就得到了jwt的第二部分,所以这部分是明文的base64。
3.签名signature
最后是第三部分,这部分是通过前两步,将base64(header) + '.' + base64(payload),构造成字符串,然后用header中的算法做加密,得到摘要digest,再保存成base64编码,然后拼在后面,即得到了一个完成的jwt字串。
所以就是验证前两部分的内容获得的签名是否一致,一致就表示内容没有篡改过,数据是有效的。举例如:A构造header+payload,然后用私钥加密构造jwt;然后B得到jwt,解析到header+payload,然后用公钥验证签名是否通过。
这里就用到了非对称加密。

代码

这里使用java语言实现,首先引入依赖

       
            io.jsonwebtoken
            jjwt-api
            0.11.1
        
        
            io.jsonwebtoken
            jjwt-impl
            0.11.1
        
        
            io.jsonwebtoken
            jjwt-jackson
            0.11.1
        

构造示例

下面的例子是个举例,使用ECDSA加密。(未验证)

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.apache.commons.codec.binary.Base64;
import org.joda.time.Hours;
import org.apache.commons.io.FileUtils;
    /**
     * {
     * "alg": "ES256",
     * "kid": "YOUR123KID"  //id是10位
     * }
     * {
     * "iss": "YOURTEAMID",
     * "iat": 1600853455,
     * "exp": 1606123855,
     * "aud": "https://appleid.apple.com",
     * "sub": "com.mytest.app"
     * }
     * 私钥加密后给苹果去验证
     */
    public String buildJwt() {
        Map header = new HashMap<>();
        header.put("alg", "ES256");
        header.put("kid", "YOUR123KID");

        long iat = System.currentTimeMillis() / 1000;
        Map claims = new HashMap<>();
        claims.put("iss", "YOURTEAMID");
        claims.put("iat", iat);
        claims.put("exp", iat + Hours.EIGHT.toStandardSeconds().getSeconds());
        claims.put("aud", "https://appleid.apple.com");
        claims.put("sub", "com.mytest.app");
        JwtBuilder jwtBuilder =new DefaultJwtBuilder()
                .setHeader(header)
                .setClaims(claims)
                .signWith(getPrivateKey(),SignatureAlgorithm.ES256);
        return jwtBuilder.compact();
    }
    
        private static Key getPrivateKey() {
            try {
                String file = "PATH::YOUR123KID.p8";
                List lines = FileUtils.readLines(new File(file), StandardCharsets.UTF_8);
                StringBuilder keyValue = new StringBuilder();
                for (String s : lines) {
                    if (s.startsWith("---")) {
                        continue;
                    }
                    keyValue.append(s);
                }
                KeyFactory factory = KeyFactory.getInstance("EC");
                EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(keyValue.toString().replaceAll("\\n", "")));
                PrivateKey privateKey = factory.generatePrivate(keySpec);
                System.out.println(privateKey);
                return privateKey;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

验证示例

//import
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.apache.commons.codec.binary.Base64;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

    //从apple获取公钥
    public static ApplePubKeys getApplePublicKey() {
        try { 
            final String applePkEndpoint = "https://appleid.apple.com/auth/keys";
            String resp = HTTP_GET(applePkEndpoint);
            ApplePubKeys keys = new Gson().fromJson(resp, ApplePubKeys.class);
            return keys;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ApplePubKeys();
    }

   /**
     * @param modulus  模数 n
     * @param exponent 指数 e
     * @return
     */
    public static PublicKey getPublicKey(String modulus, String exponent) {
        try {
            BigInteger bigModule = new BigInteger(1, Base64.decodeBase64(modulus));
            BigInteger bigExponent = new BigInteger(1, Base64.decodeBase64(exponent));
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigModule, bigExponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            return publicKey;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    
    
    public static void verify(String jwt) throws InvalidPublicKeyException {
        AppleIdentityToken identityToken = getAppleIdentityToken(jwt);
        ApplePublicKey applePublicKey = getApplePublicKey().getByKid(identityToken.getHeader().getKid());
        if (null == applePublicKey) {
            System.out.println("no valid public key");
            return;
        }
        PublicKey publicKey = getPublicKey(applePublicKey.getN(), applePublicKey.getE());
        JwtParser jwtParser = Jwts.parserBuilder()
                .requireAudience(identityToken.getAud())
                .requireSubject(identityToken.getSub())
                .requireIssuer("https://appleid.apple.com")
                .setSigningKey(publicKey)
                .build();

        try {
            Jws claimsJws = jwtParser.parseClaimsJws(jwt);
            if (null != claimsJws && claimsJws.getBody().containsKey("auth_time")) {
                System.out.println(claimsJws);
            } else {
                System.err.println("failed");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static AppleIdentityToken getAppleIdentityToken(String jwt) {
        String[] arr = jwt.split("\\.");
        String tokenBase64 = arr[1];
        String headerBase64 = arr[0];
        String token = new String(Base64.decodeBase64(tokenBase64));
        String header = new String(Base64.decodeBase64(headerBase64));
        AppleIdentityToken.Header tokenHeader = new Gson().fromJson(header, AppleIdentityToken.Header.class);
        AppleIdentityToken identityToken = new Gson().fromJson(token, AppleIdentityToken.class);
        identityToken.setHeader(tokenHeader);
        return identityToken;
    }
    
        //jwt转换为对象,苹果登录参数
        @Data
        static class AppleIdentityToken {
            String aud;
            String sub;
            String c_hash;
            boolean email_verified;
            long auth_time;
            String iss;
            long exp;
            long iat;
            String email;
            //
            Header header;
    
    
            @Data
            static class Header {
                String alg;
                String kid;
            }
        }
    
    
        @Data
        static class ApplePubKeys {
            List keys;
    
            public ApplePublicKey getByKid(String kid) {
                return keys.stream().filter(e -> e.getKid().equals(kid)).findFirst().orElse(null);
            }
        }
    
        //苹果登录公钥
        @Data
        public static class ApplePublicKey {
            String kty;
            String kid;
            String use;
            String alg;
            String n;
            String e;
        }

参考资料

  1. 使用appleid登录
  2. 什么是jwt
  3. jwt.io
  4. wiki
  5. RFC7519 jwt介绍

你可能感兴趣的:(jwt信息校验技术介绍)