C++ 驗證jwt(json web token)
tip: 本文主要以apple 登錄驗證舉例
一:jwt
jwt 分為 3 部分組成 base64(header) + "." + base64(payload) + "." + signature
例:
"eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmh1aXNob3ViYW8uQ29sYUdvb2RzIiwiZXhwIjoxNTg0MDAxMjM3LCJpYXQiOjE1ODQwMDA2MzcsInN1YiI6IjAwMTQ3Mi5kYWIwNGY5YmE5ZDM0ZjAzYWQ2NDFmYTgyZDFjNTk4Yi4wOTQ1Iiwibm9uY2UiOiJFb0l3WmV2bUZJIiwiY19oYXNoIjoiZVkzUzF3ZVhzQzAxTFAzSmVZdTh1dyIsImVtYWlsIjoieWY3M3BzOGhtM0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU4NDAwMDYzNywibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.wIZKE8N5xcHqql2ZqMs_bza-nakfCKqhh332ZRqPbjIc4vYstfUoIo4xWG0ALSBvcM3nNPwOA-tM3oQaD6VObOEYFlATSF_9z6WR0MTlryFkJS_DEvy_VMvcXBzEun9ccdx-21WxXmkQy74TxEM4Wl_kZMsjNVdto0Pw0oMGi7Zio52JthO7Q5pKnOpYoGiTJRi0L2egh6BXm72he9XYu6PbZBnwr_Gf-S05bA5EMFXD896DciKtGeWrw91f7DBqqmXTUqlBUuSvFgbVq6UftkmVqjrIIAb9r5I42QayToOTHQAzcxk8ena7OnY16EKK4FLZyHx1DW87_7zLDg6-SQ"
1.將header base64.decode
{
"kid": "eXaunmL",
"alg": "RS256"
}
2.將 payload base64.decode
{
"iss": "https://appleid.apple.com",
"aud": "com.huishoubao.ColaGoods",
"exp": 1584001237,
"iat": 1584000637,
"sub": "001472.dab04f9ba9d34f03ad641fa82d1c598b.0945",
"nonce": "EoIwZevmFI",
"c_hash": "eY3S1weXsC01LP3JeYu8uw",
"email": "yf73ps8hm3@privaterelay.appleid.com",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1584000637,
"nonce_supported": true
}
4.最后一部分就是剩下的signature
signature的驗證是 base64UrlEncode(header) + "." + base64UrlEncode(payload)
二.JWK (json web key ) To Pem
c++ 通過JWK 對JWT 進行驗證,首先需要將JWK 轉換成 PEM 格式的
從蘋果接口獲取JWK :https://appleid.apple.com/auth/keys
{
"keys": [
{
"kty": "RSA",
"kid": "86D88Kf",
"use": "sig",
"alg": "RS256",
"n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "eXaunmL",
"use": "sig",
"alg": "RS256",
"n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw",
"e": "AQAB"
}
]
}
該JWK 中 kid 表示 jwt 使用的加密串, n 為 rsa 的模數,e 為rsa 的指數,alg 為加密的算法
n,e 兩個字符串特別重要,需要用它來生成我們的rsa 公鑰
C++ 沒有找到比較好的現成的轉換的庫,所以使用openssl 進行生成
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/md5.h>
#include <openssl/des.h>
#include <openssl/pkcs12.h>
#include "cppcodec/base64_url_unpadded.hpp"
bool ConvertJwkToPem(const std::string &strnn, const std::string& strnn, std::string& strPubKey){
auto nn = cppcodec::base64_url_unpadded::decode(strnn);
auto ee = cppcodec::base64_url_unpadded::decode(stree);
BIGNUM * modul = BN_bin2bn(nn.data(), nn.size(), NULL);
BIGNUM * expon = BN_bin2bn(ee.data(), ee..size(), NULL);
RSA * rr = RSA_new();
EVP_PKEY * pRsaKey = EVP_PKEY_new();
rr->n = modul;
rr->e = expon;
EVP_PKEY_assign_RSA(pRsaKey, rr);
unsigned char * ss = new unsigned char[1024];
unsigned char *desc = new unsigned char[1024];
memset(desc, 0, 1024);
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSA_PUBKEY(bio, rr);
BIO_read(bio, desc, 1024);
strPubKey = (char*)desc;
BIO_free(bio);
RSA_free(rr);
delete[] ss;
delete[] desc;
if (strPubKey.empty()) {
return false;
}
return true;
}
在這里推薦一個cpp 好用的工具庫 cppcodec ,只需要包含頭文件就可以了,不用編譯,不用鏈接第三方動態庫
三:驗證JWT
在上面拿到了publickey 之后后面的就簡單了,規則為 RS256(base64UrlEncode(header) + "." + base64UrlEncode(payload) + publickey)加密后生成的密文 與 signature 進行比較,相等的話則驗證通過。下面貼出一段驗證代碼示例:同樣的,為了方便,也是用到了一個開源庫
bool verifyJwt(const std::string &strJwtToken, const std::string& strPubKey){
try
{
auto decoded = jwt::decode(strJwtToken);
auto verifie = jwt::verify().allow_algorithm(jwt::algorithm::rs256(strPubkey, "", "", "")).with_issuer("auth0");
bool bok = verifie.verify(decoded);
}
catch (jwt::signature_verification_exception *e)
{
std::string strException = e->what();
INFO("verification_exception:" + strException);
m_strRet = "1";
m_strRetCode = tools::CStringTools::Int2String(error_num::PARAMETER_ERROR);
m_strRetInfo = error_string::ErrorStr(error_num::PARAMETER_ERROR);
return false;
}
catch (...)
{
INFO("identityToken error" );
m_strRet = "1";
m_strRetCode = tools::CStringTools::Int2String(error_num::PARAMETER_ERROR);
m_strRetInfo = error_string::ErrorStr(error_num::PARAMETER_ERROR);
return false;
}
}
以上代碼我將開源庫部分代碼改了方便使用
四.參考文章
https://blog.csdn.net/xiaoting451292510/article/details/46729377
https://www.cnblogs.com/tml839720759/p/3926006.html
https://stackoverflow.com/questions/57217529/how-to-convert-jwk-public-key-to-pem-format-in-c
五:用到的開源庫
openssl :
cppcodec:https://www.ctolib.com/article/goGitHub/cppcodec.html