集成Singpass授权登录

922 阅读4分钟

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_endpointtoken获取API
token_endpoint_auth_signing_alg_values_supported获取token是支持的签名算法
id_token_encryption_alg_values_supportedid_token支持的加密算法
token_endpoint_auth_methods_supported获取token支持的两种方式
image.png 不过根据官网的意思,client_secret_post 这个方式已经被弃用了!我就是这里大意了!

JWK URL

其实上述内容已经准备好了,没多少东西,这个集成的第一个难点就是JWK的配置,为什么要用JWK呢?

  1. clinet_secret 的方式已经弃用了,取而代之的是client_assertion
  2. client_assertion 需要我们客户端签名,那么singpass如何验证签名呢
  3. id_token 根据官网它是一个JWE,他们是如何加密的呢,我们得到以后如何解密呢

这就是JWK 的作用和意义,用来解决这三个问题!

获取token需要的参数(client_assertion最为难办):

image.png

client_assertion 支持的签名算法,就是说你对client_assertion签名,必须使用singpass列出来的签名算法! image.png

这里要注意,claims是组成整个签名原文的主要内容,其中要求exp不能大于lat超过两分钟!!! image.png

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

jwks-verifier

image.png

验证通过以后再继续后面的工作,否则可能是瞎忙活😭

签名

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的格式和作用.这个呢还算简单,就不叙述了😆