修改google play登录

This commit is contained in:
李运家 2025-01-23 11:45:36 +08:00
parent 0ce2c57103
commit 6f04f32161
5 changed files with 254 additions and 71 deletions

107
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -264,6 +264,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.39"
@ -1022,6 +1028,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
@ -1368,6 +1375,7 @@ dependencies = [
"lazy_static",
"macro",
"moka",
"oauth2",
"once_cell",
"redis",
"reqwest",
@ -1673,6 +1681,26 @@ dependencies = [
"libc",
]
[[package]]
name = "oauth2"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
dependencies = [
"base64 0.22.1",
"chrono",
"getrandom",
"http",
"rand",
"reqwest",
"serde",
"serde_json",
"serde_path_to_error",
"sha2",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "object"
version = "0.36.7"
@ -1932,6 +1960,58 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quinn"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.11",
"tokio",
"tracing",
]
[[package]]
name = "quinn-proto"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
dependencies = [
"bytes",
"getrandom",
"rand",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.11",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "quote"
version = "1.0.38"
@ -2078,7 +2158,10 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
@ -2086,12 +2169,14 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"windows-registry",
]
@ -2136,6 +2221,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -2165,6 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
@ -2185,6 +2277,9 @@ name = "rustls-pki-types"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
dependencies = [
"web-time",
]
[[package]]
name = "rustls-webpki"
@ -3315,6 +3410,7 @@ dependencies = [
"form_urlencoded",
"idna 1.0.3",
"percent-encoding",
"serde",
]
[[package]]
@ -3530,6 +3626,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.5.2"

View File

@ -62,3 +62,4 @@ redis = "0.27.6"
deadpool-redis = "0.18.0"
chrono-tz = "0.10.0"
inventory = "0.3.17"
oauth2 = "5.0.0"

View File

@ -40,6 +40,7 @@ redis = { workspace = true, features = ["tokio-comp", "json"] }
deadpool-redis = { workspace = true }
chrono-tz = { workspace = true }
inventory = { workspace = true }
oauth2 = { workspace = true, features = ["reqwest"]}
domain = { path = "../domain" }
i18n = { path = "../i18n" }

View File

@ -62,11 +62,17 @@ pub struct Redis {
#[derive(Clone, Debug, Deserialize)]
pub struct Social {
pub wechat: Wechat,
// pub google: google::GoogleSocial,
pub google: Google,
// pub facebook: facebook::FacebookSocial,
// pub apple: apple::AppleSocial,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Google {
pub client_id: String,
pub client_secret: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Wechat {
pub app_id: String,

View File

@ -4,10 +4,17 @@ use chrono::Utc;
use futures_util::lock::Mutex;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use lazy_static::lazy_static;
use oauth2::basic::BasicClient;
use oauth2::{reqwest, RevocationUrl};
use oauth2::{
AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
Scope, TokenResponse, TokenUrl,
};
use reqwest::Client;
use serde::Deserialize;
use serde_json::Value;
use crate::config;
use crate::model::response::ResErr;
use super::SocialResult;
@ -133,93 +140,110 @@ impl GoogleSocial {
// 根据kid找到正确的公钥
let key = public_keys
.get(&kid)
.ok_or_else(|| Box::new(ResErr::social("校验Token失败,未找到正确的公钥")))?;
.get(&kid);
tracing::debug!("public key : {:?}", key);
// TODO:
// 如果是google game account可能找不到正确的公钥需要使用oauth2开发库来实现相应的校验
// oauth2开发库参考https://github.com/ramosbugs/oauth2-rs/blob/main/examples/google.rs
// google game account账户校验参考https://github.com/heroiclabs/nakama/blob/master/social/social.go
// 验证Token
let mut validation: Validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&["https://accounts.google.com", "accounts.google.com"]); // 设置预期的发行者
validation.validate_aud = false;
match key {
Some(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::<Value>(
id_token,
&DecodingKey::from_rsa_components(key.n.as_str(), key.e.as_str()).unwrap(),
&validation,
)?;
let decoded = decode::<Value>(
id_token,
&DecodingKey::from_rsa_components(key.n.as_str(), key.e.as_str()).unwrap(),
&validation,
)?;
let claims: Value = decoded.claims;
let google_jwt_profile = GoogleJwtProfile::from(claims);
let claims: Value = decoded.claims;
let google_jwt_profile = GoogleJwtProfile::from(claims);
// 校验有效期
if google_jwt_profile.exp < Utc::now().timestamp() {
return Err(Box::new(ResErr::social("校验Token失败,token有效期无效")));
// 校验有效期
if google_jwt_profile.exp < Utc::now().timestamp() {
return Err(Box::new(ResErr::social("校验Token失败,token有效期无效")));
}
Ok(google_jwt_profile)
}
None => {
self.verify_play_games_token(id_token).await
}
}
Ok(google_jwt_profile)
}
/// 验证 Google Play Games 的身份信息
/// 参考: https://developers.google.com/games/services/android/signin
pub async fn verify_play_games_token(&self, play_games_token: &str) -> SocialResult<GoogleJwtProfile> {
// Google Play Games 的验证端点
const GOOGLE_PLAY_GAMES_TOKEN_INFO_URL: &str =
"https://oauth2.googleapis.com/tokeninfo";
/// 使用 OAuth2 token exchange 获取有效的 access token
/// oauth2开发库参考https://github.com/ramosbugs/oauth2-rs/blob/main/examples/google.rs
/// google game account账户校验参考https://github.com/heroiclabs/nakama/blob/master/social/social.go
/// TODO: redirect_uri 需要改为实际的回调地址
pub async fn verify_play_games_token(
&self,
play_games_token: &str,
) -> SocialResult<GoogleJwtProfile> {
// 1. 创建 OAuth2 client
let client = BasicClient::new(ClientId::new(config!().social.google.client_id.clone()))
.set_client_secret(ClientSecret::new(config!().social.google.client_secret.clone()))
.set_auth_uri(AuthUrl::new(
"https://accounts.google.com/o/oauth2/v2/auth".to_string(),
)?)
.set_token_uri(TokenUrl::new(
"https://oauth2.googleapis.com/token".to_string(),
)?)
.set_redirect_uri(RedirectUrl::new(
"http://localhost:8080/auth/google/callback".to_string(),
)?)
// Google supports OAuth 2.0 Token Revocation (RFC-7009)
.set_revocation_url(
RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string())
.expect("Invalid revocation endpoint URL"),
);
let client = Client::new();
let response = client
.get(GOOGLE_PLAY_GAMES_TOKEN_INFO_URL)
.query(&[("access_token", play_games_token)])
.send()
// Google supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/).
// Create a PKCE code verifier and SHA-256 encode it as a code challenge.
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
// Generate the authorization URL to which we'll redirect the user.
let (_authorize_url, _csrf_state) = client
.authorize_url(CsrfToken::new_random)
// This example is requesting access to the "calendar" features and the user's profile.
.add_scope(Scope::new(
"https://www.googleapis.com/auth/calendar".to_string(),
))
.add_scope(Scope::new(
"https://www.googleapis.com/auth/plus.me".to_string(),
))
.set_pkce_challenge(pkce_code_challenge)
.url();
// 2. 执行 token exchange
let token_response = client
.exchange_code(AuthorizationCode::new(play_games_token.to_string()))
.set_pkce_verifier(pkce_code_verifier)
.request_async(&reqwest::Client::new())
.await?;
if !response.status().is_success() {
return Err(Box::new(ResErr::social("验证 Google Play Games Token 失败")));
let access_token = token_response.access_token();
// 3. 使用新的 access token 获取用户信息
let mut profile = self.fetch_play_games_profile(access_token.secret()).await?;
// 4. 获取额外的用户信息(如果需要)
if let Some(additional_info) = self
.fetch_additional_player_info(access_token.secret())
.await?
{
profile.google_play_games_profile = Some(serde_json::to_string(&additional_info)?);
}
let token_info: Value = response.json().await?;
// 验证 token 信息
if let Some(error) = token_info.get("error") {
return Err(Box::new(ResErr::social(format!(
"Google Play Games Token 无效: {}",
error
))));
}
// 验证 audience 是否匹配你的应用
if let Some(aud) = token_info.get("aud") {
// TODO: 验证 aud 是否匹配你的 Google Play Games 应用 ID
tracing::debug!("Google Play Games aud: {}", aud);
}
// 构建用户资料
let mut profile = GoogleJwtProfile::default();
if let Some(sub) = token_info.get("sub").and_then(|v| v.as_str()) {
profile.sub = sub.to_string();
}
if let Some(email) = token_info.get("email").and_then(|v| v.as_str()) {
profile.email = email.to_string();
}
// 获取 Google Play Games 特定的信息
let play_games_info = self.fetch_play_games_profile(play_games_token).await?;
profile.google_play_games_id = play_games_info.get("playerId")
.and_then(|v| v.as_str())
.map(String::from);
Ok(profile)
}
/// 获取 Google Play Games 用户资料
async fn fetch_play_games_profile(&self, access_token: &str) -> SocialResult<Value> {
const PLAY_GAMES_PROFILE_URL: &str =
"https://games.googleapis.com/games/v1/players/me";
async fn fetch_play_games_profile(&self, access_token: &str) -> SocialResult<GoogleJwtProfile> {
const PLAY_GAMES_PROFILE_URL: &str = "https://games.googleapis.com/games/v1/players/me";
let client = Client::new();
let response = client
@ -229,10 +253,56 @@ impl GoogleSocial {
.await?;
if !response.status().is_success() {
return Err(Box::new(ResErr::social("获取 Google Play Games 用户资料失败")));
return Err(Box::new(ResErr::social(
"获取 Google Play Games 用户资料失败",
)));
}
let player_info: Value = response.json().await?;
// 构建用户资料
let mut profile = GoogleJwtProfile::default();
// 设置基本信息
if let Some(player_id) = player_info.get("playerId").and_then(|v| v.as_str()) {
profile.sub = player_id.to_string();
profile.google_play_games_id = Some(player_id.to_string());
}
// 设置名称
if let Some(display_name) = player_info.get("displayName").and_then(|v| v.as_str()) {
profile.name = display_name.to_string();
}
// 设置头像
if let Some(avatar_image_url) = player_info.get("avatarImageUrl").and_then(|v| v.as_str()) {
profile.picture = avatar_image_url.to_string();
}
let profile: Value = response.json().await?;
Ok(profile)
}
/// 获取额外的玩家信息
async fn fetch_additional_player_info(
&self,
access_token: &str,
) -> SocialResult<Option<Value>> {
const PLAY_GAMES_PLAYER_STATS_URL: &str =
"https://games.googleapis.com/games/v1/players/me/playerStates";
let client = Client::new();
let response = client
.get(PLAY_GAMES_PLAYER_STATS_URL)
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
if !response.status().is_success() {
tracing::warn!("获取额外玩家信息失败: {}", response.status());
return Ok(None);
}
let stats: Value = response.json().await?;
Ok(Some(stats))
}
}