From 22f7649bca0741db4e819846466b16321b95bda8 Mon Sep 17 00:00:00 2001 From: liyunjia Date: Wed, 29 May 2024 21:19:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0apple=20id=20token=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/src/social/apple.rs | 193 ++++++++++++++++++++++++++++++++++++ service/src/account.rs | 3 +- 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/library/src/social/apple.rs b/library/src/social/apple.rs index e69de29..28daba5 100644 --- a/library/src/social/apple.rs +++ b/library/src/social/apple.rs @@ -0,0 +1,193 @@ +use std::{collections::HashMap, sync::Arc}; + +use chrono::Utc; +use futures_util::lock::Mutex; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use lazy_static::lazy_static; +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; + +use crate::resp::response::ResErr; + +use super::SocialResult; + +lazy_static! { + pub static ref APPLE_SOCIAL: AppleSocial = AppleSocial::default(); +} + +#[derive(Default)] +pub struct AppleSocial { + apple_public_keys: Arc>, +} + +// 假设APPLE_PUBLIC_CERT_URL是Apple提供的公钥URL +const APPLE_PUBLIC_CERT_URL: &str = "https://appleid.apple.com/auth/keys"; + +#[derive(Debug, Default)] +pub struct AppleJwtProfile { + // iss (issuer):签发人 + pub iss: String, + // sub (subject):主题 + pub sub: String, + pub azp: String, + // aud (audience):受众 + pub aud: String, + // iat (Issued At):签发时间 + pub iat: i64, + // exp (expiration time):过期时间 + pub exp: i64, + pub email: String, + pub email_verified: bool, + pub at_hash: String, + pub name: String, + pub picture: String, + pub given_name: String, + pub family_name: String, + pub locale: String, +} + +impl AppleJwtProfile { + fn new() -> Self { + AppleJwtProfile { + ..Default::default() + } + } +} + +impl From for AppleJwtProfile { + fn from(value: Value) -> Self { + let mut apple_jwt_profile = AppleJwtProfile::new(); + if let Some(value) = value.get("iss") { + apple_jwt_profile.iss = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("sub") { + apple_jwt_profile.sub = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("azp") { + apple_jwt_profile.azp = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("aud") { + apple_jwt_profile.aud = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("iat") { + apple_jwt_profile.iat = value.as_i64().unwrap_or_default(); + } + if let Some(value) = value.get("exp") { + apple_jwt_profile.exp = value.as_i64().unwrap_or_default(); + } + if let Some(value) = value.get("email") { + apple_jwt_profile.email = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("email_verified") { + apple_jwt_profile.email_verified = value.as_bool().unwrap_or_default(); + } + if let Some(value) = value.get("at_hash") { + apple_jwt_profile.at_hash = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("name") { + apple_jwt_profile.name = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("picture") { + apple_jwt_profile.picture = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("given_name") { + apple_jwt_profile.given_name = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("family_name") { + apple_jwt_profile.family_name = value.as_str().unwrap().to_string(); + } + if let Some(value) = value.get("locale") { + apple_jwt_profile.locale = value.as_str().unwrap().to_string(); + } + apple_jwt_profile + } +} + +#[derive(Deserialize, Debug, Clone, Default)] +struct ApplePublicKey { + e: String, + n: String, + kty: String, + #[serde(rename = "use")] + use_: String, + alg: String, + kid: String, +} + +#[derive(Deserialize, Debug, Clone, Default)] +struct ApplePublicKeys { + keys: Vec, + #[serde(skip)] + refresh_at: i64, +} + +impl AppleSocial { + // 异步获取并解析Apple公钥 + async fn fetch_and_parse_apple_public_keys(&self) -> SocialResult> + { + let mut public_keys = self.apple_public_keys.lock().await; + if public_keys.keys.is_empty() || public_keys.refresh_at < Utc::now().timestamp() { + let response = Client::new().get(APPLE_PUBLIC_CERT_URL).send().await?; + let mut apple_keys: ApplePublicKeys = response.json().await?; + tracing::info!("Apple公钥获取成功, {:?}", apple_keys); + apple_keys.refresh_at = Utc::now().timestamp() + 3600; + *public_keys = apple_keys; + } + + let mut key_map = HashMap::new(); + // 解析公钥 + for key in public_keys.keys.iter() { + if key.kty == "RSA" { + key_map.insert(key.kid.to_owned(), key.to_owned()); + } + } + + tracing::info!("Apple公钥解析成功, {:?}", key_map); + Ok(key_map) + } + + // 验证ID Token并考虑kid匹配 + pub async fn verify_id_token(&self, id_token: &str) -> SocialResult { + // 获取并解析公钥 + let public_keys = self.fetch_and_parse_apple_public_keys().await?; + + // 解码Token头部以获取kid + let token_header = jsonwebtoken::decode_header(id_token).unwrap(); + let kid = token_header.kid; + + // 检查是否找到了有效的kid + if kid.is_none() { + return Err(Box::new(ResErr::social("校验Token失败,未找到有效的kid"))); + } + let kid = kid.unwrap(); + + // 根据kid找到正确的公钥 + let key = public_keys + .get(&kid) + .ok_or_else(|| Box::new(ResErr::social("校验Token失败,未找到正确的公钥")))?; + + tracing::info!("public key : {:?}", key); + + // 验证Token + let mut validation: Validation = Validation::new(Algorithm::RS256); + // validation.set_issuer(&["https://accounts.google.com", "accounts.google.com"]); // 设置预期的发行者 + validation.validate_aud = false; + + let decoded = decode::( + id_token, + &DecodingKey::from_rsa_components(key.n.as_str(), key.e.as_str()).unwrap(), + &validation, + )?; + + let claims: Value = decoded.claims; + let apple_jwt_profile = AppleJwtProfile::from(claims); + + // 校验有效期 + if apple_jwt_profile.exp < Utc::now().timestamp() { + return Err(Box::new(ResErr::social("校验Token失败,token有效期无效"))); + } + + Ok(apple_jwt_profile) + } +} diff --git a/service/src/account.rs b/service/src/account.rs index f52f1ba..36988f5 100644 --- a/service/src/account.rs +++ b/service/src/account.rs @@ -1,13 +1,14 @@ use chrono::Utc; use domain::dto::account::AuthenticateGooleAccountReq; use domain::entities::account::Account; +use library::social::apple::APPLE_SOCIAL; use library::{db, token}; use library::resp::response::ResErr::ErrPerm; use library::resp::response::{ResErr, ResOK, ResResult}; use library::social::google::GOOGLE_SOCIAL; pub async fn authticate_google(req: AuthenticateGooleAccountReq) -> ResResult> { - let verify_result = GOOGLE_SOCIAL.verify_id_token(&req.id_token.unwrap()).await.map_err(|err| { + let verify_result = APPLE_SOCIAL.verify_id_token(&req.id_token.unwrap()).await.map_err(|err| { tracing::error!(error = ?err, "校验Google Token失败"); ErrPerm(None) })?;