JYM大家好,这里是布洛妮娅, 鸭鸭的工作笔记频道😎!!
第三方登录集成Singpass
前言
SingPass新加坡政府技术局Gov Tech管理。它是每个新加坡居民的数字身份,就是你的NRIC号码或者FIN号码。 每个SingPass账号持有人可以轻松访问多个新加坡政府机构的服务部门,安全登陆及访问政府及私营部门的服务。
本鸭上周加班了,整整四天!!就是接入Singpass登录,本来我以为不就是三个API吗,我轻敌了呀!🤣🤣
Singpass API
//访问这个接口这个是测试环境的地址,生产环境去掉stg即可
GET https://stg-id.singpass.gov.sg/.well-known/openid-configuration?=
得到的结果
{
"issuer": "https://stg-id.singpass.gov.sg",
"authorization_endpoint": "https://stg-id.singpass.gov.sg/auth",
"jwks_uri": "https://stg-id.singpass.gov.sg/.well-known/keys",
"response_types_supported": [
"code"
],
"scopes_supported": [
"openid"
],
"subject_types_supported": [
"public"
],
"claims_supported": [
"nonce",
"aud",
"iss",
"sub",
"exp",
"iat"
],
"grant_types_supported": [
"authorization_code",
"urn:openid:params:grant-type:ciba"
],
"token_endpoint": "https://stg-id.singpass.gov.sg/token",
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"ES256",
"ES384",
"ES512"
],
"id_token_signing_alg_values_supported": [
"ES256"
],
"id_token_encryption_alg_values_supported": [
"ECDH-ES+A256KW",
"ECDH-ES+A192KW",
"ECDH-ES+A128KW",
"RSA-OAEP-256"
],
"id_token_encryption_enc_values_supported": [
"A256CBC-HS512"
],
"backchannel_authentication_endpoint": "https://stg-id.singpass.gov.sg/bc-auth",
"backchannel_token_delivery_modes_supported": [
"poll"
]
}
参数说明
key | 说明 |
---|---|
authorization_endpoint | 授权码获取API |
token_endpoint | token获取API |
token_endpoint_auth_signing_alg_values_supported | 获取token是支持的签名算法 |
id_token_encryption_alg_values_supported | id_token支持的加密算法 |
token_endpoint_auth_methods_supported | 获取token支持的两种方式 |

JWK URL
其实上述内容已经准备好了,没多少东西,这个集成的第一个难点就是JWK的配置,为什么要用JWK呢?
- clinet_secret 的方式已经弃用了,取而代之的是client_assertion
- client_assertion 需要我们客户端签名,那么singpass如何验证签名呢
- id_token 根据官网它是一个JWE,他们是如何加密的呢,我们得到以后如何解密呢
这就是JWK 的作用和意义,用来解决这三个问题!
获取token需要的参数(client_assertion最为难办):
client_assertion 支持的签名算法,就是说你对client_assertion签名,必须使用singpass列出来的签名算法!
这里要注意,claims是组成整个签名原文的主要内容,其中要求exp不能大于lat超过两分钟!!!
JWK 内容
其实jwk url 可以简单话我们可以自己生成对应算法的公钥,私钥,将公钥转为下面的格式,私钥自己保存,下述格式的内容可以提供一个API获取,将这个API配置到申请singpass client_id的后台.
{
"keys": [
{
"kid": "mKH6bss37iVrSwzJsioT7nQJwcFX1bZ5TWVn5Btmhq4",
"kty": "RSA",
"alg": "RSA-OAEP-256",
"use": "enc",
"n": "公钥",
"e": "AQAB",
"x5c": [
"****"
],
"x5t": "***",
"x5t#S256": "***"
},
{
"kid": "EuNaqSRDepDUMUBa04Jvk8mGMJJUmZKn9b0Su_u22a4",
"kty": "EC",
"alg": "ES256",
"use": "sig",
"crv": "P-256",
"x": "****",
"y": "***"
}
]
}
验证JWK
验证通过以后再继续后面的工作,否则可能是瞎忙活😭
签名
private String getClientAssertion() {
Map<String, Object> header = new HashMap<>(2);
header.put("alg", SignatureAlgorithm.ES256);
header.put("typ", OAuth2Constants.JWT);
Map<String, Object> claims = new HashMap<>(5);
long currentTime = Time.currentTime();
claims.put("iss", "singpass 给你的clinet_id");
claims.put("iat", currentTime);
claims.put("exp", currentTime + 2 * 60);
//一开始获取到的那个issuer
claims.put("aud", "https://stg-id.singpass.gov.sg");
claims.put("sub", "singpass 给你的clinet_id");
try {
//私钥转为Base64,再使用PKCS8EncodedKeySpec编码
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(“你的ES256签名算法的私钥”));
// 实例化KeyFactory对象,并指定EC算法
KeyFactory keyFactory = KeyFactory.getInstance(“EC”);
//获得PrivateKey对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
String clientSecret = Jwts.builder().setHeader(header).setClaims(claims)
.signWith(privateKey, SignatureAlgorithm.ES256).compact();
log.info("clientSecret:\n{}", clientSecret);
return clientSecret;
} catch (Exception e) {
log.error("获取singpass client_assertion 失败!");
}
return null;
}
然后就是通过API获取Token了!!!
解析TOKEN
{ "access_token" : "m2TVy9mignZ/CG4vacRJ3R2AVPLsMLnyx37q48HPL2I=",
"token_type" : "Bearer",
"id_token" : "
eyJraWQiOiJtS0g2YnNzMzdpVnJTd3pKc2lvVDduUUp3Y0ZYMWJaNVRXVm41QnRtaHE0IiwiY3R5IjoiSldUIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IlJTQS1PQUVQLTI1NiJ9.BHMARgPyTrs-6v-_Sn0bzV1EIFKCJPvg4PVK1zpoMo9JXY74gz6J5qzwrFcRPNvirmlJpWNNYkGAl4JyHhCIAxRdax-TYzMux8Ty5Je2EVlNMq2miIM24kpWPQI-Q8SnNHAjNqyC5T6-WsjK_vARMaCZ5gRM1ox6m8a2e3f5gs65TRrISSOQku3ljCKIn5BE-eKih54SFltEUZZ2-sAm2hAyknpy2UUu7EH_lWrdqqz-z8vbnJ2ZtRpptFS-ZPwECu2opozOB64hRbhraE7uBNd2ofR7GHWOUWrjVDmHe9Hp5c52Z7I-mQP-wQ9uztvvV--UWIX9D11DLIE8Xpz15w.nzMgN5DtQYcRH3-XNaxy6A.U5BPo1ZeBB5ZOXkrWbOStDRQY2vUaL6rjzXdfSj2fCMNR5LAGo8WXj1uYrZ_tUTzvMIi0HW_PS9le6QPrv-LXSj6_rAD0nWZS7m3R6BtTuzywSFoYWrGhTyXWNiwTUG6vdkWD6QfynGxfsxBGMGNRwlvNjIc0Q3Dbvs-eCb0_JDuLKLC117-lPpCr5NYIyCPxf8bNvoK0yNPm004iWzf4L5-K-3uIB9RuzjWSOp-Xhf8Ibz4_cSnnIBZ8naMUWZ0-bK1Mfjq0ejQkz3RAeCCXpeTeSHhqjNoBdZH2j0ajDGA2BqPuHrdUBbAV9MnLLWC_PyaTZfMSmr2yyspo9fkarpQs-XOj2SfzcRXQvFNtst0aezUPIMv_OeUizmZEK02doV1XpKJZciUMkLw0MJsIsfHHiGnqO1IKVyurK7vQrtsApgGKJjXDTQpShCh_fEXMnEhR3rlaEdNNR9C_v1neG1WnpsBKm0OVt_agA1HjqIQVgR1nB5DAMlxNJezpd7W5JUIm1580nR0zPjG9uSb0Ths5l4Kp-jkbHPMXpuE0x_-QiNdoI7BRNJj1dAWwq6wGMXlh2RgRNCEDshHHrgxvWceOrsOTaLlJ1pd41A-smStvbKgBoY0b0_cwabUbPD-.OG1iY5cC-xaKuDR2yyLVrOaHeG1aTLJFzz9vN1rv0cQ
"
}
得到了正确的内容,大家千万记住别用官网的那个示例做解析,格式根本和实际的不一样,实际是一个JWE,有五个部分,用 . 隔开,关于JWE的内容大家自行去学习,不难.
解析用到的包(亲测最好使的包):
<dependency>
<groupld>com.nimbusds<groupld>
<artifactid>nimbus-jose-jwt<artifactid>
<version>9.25.6</version>
</dependency>
try {
Security.addProvider(new BouncyCastleProvider());
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode("你的RSA算法的私钥(加密算法不是签名算法)"));
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey pk = kf.generatePrivate(spec);
EncryptedJWT jwt = EncryptedJWT.parse(jweString);
RSADecrypter decrypter = new RSADecrypter(pk);
jwt.decrypt(decrypter);
Payload payload = jwt.getPayload();
String[] arr = payload.toString().split("\.");
//五个部分都是用Base64编码后用.拼接成完成的id_token,而我们需要的内容在第二部分
String decode = new String(Base64.getDecoder().decode(arr[1]));
String substring = decode.substring(0, decode.indexOf("}") + 1);
System.out.println("substring = " + substring);
} catch (Exception e) {
System.out.println(e.getMessage());
}
总结
一定要认真去读官方的文档,虽然例子可能下了毒,但是文字肯定没撒谎.🤣 最主要的还是真正了解JWK的用途,我们用私钥来签名,对方用公钥去验证,对方用公钥加密token,我们用私钥解密token,在私钥相对安全的情况下完成了签名,加密的过程. 其次就是了解JWT,JWS,JWE的格式和作用.这个呢还算简单,就不叙述了😆