企业服务平台的token,可以在应用系统的本地进行验签,这样的可以提升接口性能,不需要每次都进行验签。方案如下:
1、公钥获取;
公钥获取,做好缓存机制,下文有介绍,不需要频繁调用;
2、令牌验签;
使用平台公钥进行令牌验签,下文有介绍,可以加缓存机制;
生产环境:https://esp.xkw.cn/.well-known/openid-configuration
沙箱测试环境:https://esp.doc.xkw.cn/.well-known/openid-configuration
{
"issuer": "https://esp.xkw.cn",
"authorization_endpoint": "https://esp.xkw.cn/oauth2/authorize",
"token_endpoint": "https://esp.xkw.cn/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_post"
],
"jwks_uri": "https://esp.xkw.cn/oauth2/jwks",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
],
"code_challenge_methods_supported": [
"S256"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}
生产环境:https://esp.xkw.cn/oauth2/jwks
沙箱测试环境:https://esp.doc.xkw.cn/oauth2/jwks
{
"keys": [
{
"kty": "RSA",
"kid": "380ce45a",
"use": "sig",
"alg": "RS256",
"n": "6Zlo3NEfG5Nv1PFWxRLqYMbLSyHAk6MN1x5mCRttqdziah6V8jKPOqrzOCqQPvF9zJw5U7ghBgdGkPQs_w_yfR2PZHM4C1x7dFSgaSKRRG3vpiL5nSwu9qTfzdCo7OG8xhX5eL2P-5Uish6h5LiAWO_uOflizR-sVRC4dONCC_MSjtGptVlZJGmpA-Vr-aFqgzmyaX2oMpxwj7CJ0bx0fSBMhgRJ5xdlWl4wV3OWIDiQs9C2Y0dtF957b0RQ_YUyigfAg0SRpaAXVGt10L8ysHIj52h8men4wL7CWraZscNj_2NFdt4g5ERbihduuvNfdQ3BB9z74QIOn3n1vsRxBQ==",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "e98929a1",
"use": "sig",
"alg": "RS256",
"n": "04csr2R7dcF2xX2zlB7hX4EqD0lmoHxGDSKWdh3rKEtZUT7aN8v-dJnNI5S2zk_I8mbtQAIYUOhJR7XaEgHl2KuHeQrgN0--W-Id1Io9E7JPSVV3nzIFnLK2v2ZndRfBegHGtHLBSd14Pvwnq-gyiWhAcnYSKr0doL_LhaullgDXZjY6x700EFoKpoQjmPHFIuN-1q_u41XD4GEyFd1wmVifTR_ObcpC1JdXQpWLd3ejiaq9HPsgIDN7eC3mGWQVxpDKEaXeSNkqGKnjWzYR5FNYSX3QZNOzOZf6azN-1qDxWx_25eOnb3w7QK9kuqi_pNsnVUGz5Dee_wo9p31x2w==",
"e": "AQAB"
}
]
}
应用系统API,接口提供方需要定期获取公钥配置,可以在应用系统启动时获取最新,后续可以定时任务进行更新可以两小时更新一次;
注意:不要频繁请求获取公钥,以免被限流控制;
根据公钥接口获取最新公钥配置,实例化公钥实例,这个不同的开发语言写法不一样,可以参考网上文章,下面为java的例子代码:
public static PublicKey generatePublicKey(String base64Modulus, String base64Exponent) {
try {
// Base64 解码模数和指数
byte[] modulusBytes = Base64.getUrlDecoder().decode(base64Modulus);
byte[] exponentBytes = Base64.getUrlDecoder().decode(base64Exponent);
// 将字节数组转换为 BigInteger,在最前面加个0,防止负数
BigInteger modulus = new BigInteger(1, modulusBytes);
BigInteger exponent = new BigInteger(1, exponentBytes);
// 创建 RSAPublicKeySpec
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
// 获取 KeyFactory 实例并生成 PublicKey
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
return null;
}
}
使用公钥实例,结合jwt第三方库(nimbus-jose-jwt)进行签名验证,不同语言写法不一样,可以参考网上文章,下面为java的例子代码:
SignedJWT jwt = SignedJWT.parse(token);
JWSHeader jwtHeader = jwt.getHeader();
// 平台公钥配置反序列化对象Jwk
for(Jwk jwk:keys){
// 比对公钥kid,使用正确的公钥
if(jwk.getKid().equals(jwtHeader.getKeyID())){
// 使用公钥配置中的n(模数)和e(指数)实例化RSAPublicKey
PublicKey publicKey = PublicKeyGenerator.generatePublicKey(jwk.getN(),jwk.getE());
// 生成验证对象
RSASSAVerifier verifier = new RSASSAVerifier((RSAPublicKey)publicKey);
boolean verify = jwt.verify(verifier);
if (!verify) {
throw new RuntimeException("jwt验签失败, invalid token: " + token);
}else{
JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet();
String subject = jwtClaimsSet.getSubject();
System.out.println("jwt验签成功, 调用方: " + subject);
List<String> scopes = jwtClaimsSet.getStringListClaim("scope");
if(scopes != null && scopes.contains("bi")){
System.out.println("jwt验签成功, 允许对bi接口访问");
}else{
throw new RuntimeException("jwt验签成功, 没有授权,无法调用");
}
// 获取过期时间
Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();
if (expirationTime != null) {
// 检查 token 是否已经过期
boolean isExpired = new Date().after(expirationTime);
if (isExpired) {
throw new RuntimeException("jwt验签成功, 令牌已过期");
} else {
//接口自身业务逻辑处理
}
}
}
}
}
第一次使用平台公钥验证通过后,存储在缓存中,缓存的过期时间使用令牌的过期时间,后面调用,取缓存,如下:
缓存KEY | 缓存VALUE | 过期时间 |
---|---|---|
令牌token | { "sub": "oa", "aud": "oa", "nbf": 1710491527, "scope": ["bi", "sales"], "iss": "https://esp.doc.xkw.cn", "exp": 1710491827, "iat": 1710491527 } | 令牌的exp |