背景
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;
}
参考资料
- 使用appleid登录
- 什么是jwt
- jwt.io
- wiki
- RFC7519 jwt介绍