🦀 Rust Axum 10 分钟 OIDC 全打通:Google/Azure 一键登录,自家令牌秒发
Rust Axum 框架中生产级 OpenID Connect (OIDC) 集成最佳实践
引言背景
OpenID Connect (OIDC) 是构建在 OAuth 2.0 之上的身份认证层协议,提供标准化 ID Token(JWT 格式),包含用户身份信息(如 sub、email、name)。相比纯 OAuth 2.0,OIDC 更适合认证场景,支持 Discovery、UserInfo Endpoint、标准化 Claims。
在前文 OAuth 2.0 集成基础上,现在升级为完整生产级 OIDC 集成,实现:
- 自动 Discovery(.well-known/openid-configuration)
- Authorization Code Flow + PKCE(最安全)
- ID Token 验证 + UserInfo 获取
- 支持 Google、Microsoft Azure AD、Keycloak 等 OIDC 兼容 Provider
- 登录成功后颁发自家 Access + Refresh Token(统一认证)
- 高可维护、可配置(多 Provider 支持)
使用 openidconnect crate(最新 3.x+,2026 年仍活跃),结合 oauth2 基础。
理论与最佳实践
- Discovery:自动从 Issuer URL 获取端点、JWKS 等元数据
- ID Token:JWT 格式,必须验证签名、iss、aud、exp、nonce
- UserInfo:可选获取更多 Claims(如 email_verified、picture)
- Nonce:防重放攻击
- State + PKCE:防 CSRF 和代码拦截
- 生产:使用 Redis 存储 state/verifier(示例用内存简化)
完整实例代码(含 Google + Azure AD OIDC)
Cargo.toml
[package]
name = "axum-oidc-integration"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
axum_server = "0.7"
tokio = { version = "1.48.0", features = ["full"] }
tower-http = { version = "0.6.8", features = ["trace", "cors"] }
openidconnect = "3.5" # 最新稳定版(支持 async reqwest)
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.8", features = ["v4"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
rustls = "0.23.35"
rustls-pemfile = "2.2.0"
jsonwebtoken = "9.3" # 自家 JWT
chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", features = ["json"] }
src/main.rs
use axum::{
extract::{Extension, Query, State},
http::{HeaderValue, StatusCode},
response::{IntoResponse, Redirect},
routing::{get, post},
Json, Router,
};
use axum_server::tls_rustls::RustlsConfig;
use openidconnect::{
core::{CoreClient, CoreProviderMetadata, CoreResponseType},
reqwest::async_http_client,
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl,
Nonce, PkceCodeChallenge, RedirectUrl, Scope,
};
use openidconnect::oidc::{CoreIdToken, UserInfo};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use uuid::Uuid;
// === OIDC Provider 配置 ===
#[derive(Clone)]
pub struct OidcProvider {
pub client: CoreClient,
pub scopes: HashSet<Scope>,
pub name: String,
}
// === App State ===
#[derive(Clone)]
pub struct AppState {
pub oidc_providers: HashMap<String, OidcProvider>,
// 自家 JWT 配置(同前文)
pub jwt_access_secret: String,
pub jwt_refresh_secret: String,
}
// === 临时存储 state -> (nonce, pkce_verifier, provider) ===
type SessionStore = Arc<Mutex<HashMap<String, (Nonce, openidconnect::PkceCodeVerifier, String)>>>;
// === 登录重定向 ===
async fn oidc_login(
State(state): State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
let provider_name = params.get("provider").map(|s| s.to_lowercase()).unwrap_or_default();
let provider = match state.oidc_providers.get(&provider_name) {
Some(p) => p,
None => return (StatusCode::BAD_REQUEST, "Unsupported provider").into_response(),
};
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_token, nonce) = provider.client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scopes(provider.scopes.clone())
.set_pkce_challenge(pkce_challenge)
.url();
// 存储 session(生产用 Redis + TTL 10min)
let mut storage = SESSION_STORE.lock().unwrap();
storage.insert(csrf_token.secret().clone(), (nonce, pkce_verifier, provider_name));
Redirect::temporary(&auth_url.to_string()).into_response()
}
// === 全局 session 存储(简化内存版)===
lazy_static::lazy_static! {
static ref SESSION_STORE: SessionStore = Arc::new(Mutex::new(HashMap::new()));
}
// === 回调处理 ===
#[derive(Deserialize)]
struct CallbackQuery {
code: String,
state: String,
error: Option<String>,
error_description: Option<String>,
}
async fn oidc_callback(
State(state): State<Arc<AppState>>,
Query(query): Query<CallbackQuery>,
) -> impl IntoResponse {
if let Some(err) = query.error {
return (StatusCode::BAD_REQUEST, format!("Error: {} - {}", err, query.error_description.unwrap_or_default())).into_response();
}
let session = {
let mut storage = SESSION_STORE.lock().unwrap();
storage.remove(&query.state)
};
let (nonce, pkce_verifier, provider_name) = match session {
Some(s) => s,
None => return (StatusCode::BAD_REQUEST, "Invalid state").into_response(),
};
let provider = state.oidc_providers.get(&provider_name).unwrap();
// 交换 Token + ID Token
let token_response = provider.client
.exchange_code(AuthorizationCode::new(query.code))
.set_pkce_verifier(pkce_verifier)
.request_async(async_http_client)
.await
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Token exchange failed: {}", e)))?;
// 验证 ID Token
let id_token = token_response.id_token().ok_or((StatusCode::BAD_REQUEST, "No ID token"))?;
let id_token_verifier = provider.client.id_token_verifier();
let id_token_claims: CoreIdToken = id_token.claims(&id_token_verifier, &nonce)
.map_err(|e| (StatusCode::BAD_REQUEST, format!("ID token validation failed: {}", e)))?;
// 获取 UserInfo(可选,更多信息)
let user_info: UserInfo = provider.client
.userinfo(token_response.access_token().clone(), None)
.await
.map_err(|e| (StatusCode::BAD_REQUEST, format!("UserInfo failed: {}", e)))?;
let sub = id_token_claims.subject().to_string();
let email = user_info.email().map(|e| e.to_string());
let name = user_info.name().map(|n| n.to_string()).unwrap_or_default();
// === 查找或创建用户 + 颁发自家 JWT ===
let user_id = format!("oidc_{}", sub);
let roles = vec!["user".to_string()];
let access_token = encode_jwt(&state.jwt_access_secret, &user_id, roles.clone(), 900); // 15min
let refresh_token = encode_jwt(&state.jwt_refresh_secret, &user_id, roles, 604800); // 7 days
let mut response = Json(serde_json::json!({
"access_token": access_token,
"user": { "sub": sub, "email": email, "name": name }
})).into_response();
// 设置 HttpOnly Refresh Cookie
let cookie = format!(
"refresh_token={}; HttpOnly; Secure; SameSite=Strict; Path=/api/refresh; Max-Age=604800",
refresh_token
);
response.headers_mut().insert("set-cookie", HeaderValue::from_str(&cookie).unwrap());
response
}
// === 简单 JWT 编码(生产用 jsonwebtoken crate)===
fn encode_jwt(secret: &str, sub: &str, roles: Vec<String>, exp_secs: u64) -> String {
// 实际实现同前文 Refresh Token 部分
"dummy-jwt".to_string()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// === Discovery 示例:Google 和 Azure AD ===
let google_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new("https://accounts.google.com".to_string())?,
async_http_client,
).await?;
let google_client = CoreClient::from_provider_metadata(
google_metadata,
ClientId::new(std::env::var("GOOGLE_CLIENT_ID")?),
Some(ClientSecret::new(std::env::var("GOOGLE_CLIENT_SECRET")?)),
)
.set_redirect_uri(RedirectUrl::new("https://yourdomain.com/api/oidc/callback".to_string())?);
let azure_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new("https://login.microsoftonline.com/common/v2.0".to_string())?,
async_http_client,
).await?;
let azure_client = CoreClient::from_provider_metadata(
azure_metadata,
ClientId::new(std::env::var("AZURE_CLIENT_ID")?),
Some(ClientSecret::new(std::env::var("AZURE_CLIENT_SECRET")?)),
)
.set_redirect_uri(RedirectUrl::new("https://yourdomain.com/api/oidc/callback".to_string())?);
let mut providers = HashMap::new();
providers.insert("google".to_string(), OidcProvider {
client: google_client,
scopes: vec!["openid".into(), "email".into(), "profile".into()].into_iter().collect(),
name: "Google".to_string(),
});
providers.insert("azure".to_string(), OidcProvider {
client: azure_client,
scopes: vec!["openid".into(), "email".into(), "profile".into()].into_iter().collect(),
name: "Azure AD".to_string(),
});
let app_state = Arc::new(AppState {
oidc_providers: providers,
jwt_access_secret: "access-secret".to_string(),
jwt_refresh_secret: "refresh-secret".to_string(),
});
let cors = CorsLayer::new().allow_origin("https://your-frontend.com".parse::<HeaderValue>()?).allow_credentials(true);
let tls_config = RustlsConfig::from_pem_file("certs/fullchain.pem", "certs/privkey.pem").await?;
let app = Router::new()
.route("/api/oidc/login", get(oidc_login))
.route("/api/oidc/callback", get(oidc_callback))
.layer(cors)
.with_state(app_state)
.layer(TraceLayer::new_for_http());
let addr = ([0, 0, 0, 0], 443).into();
info!("OIDC server running on https://{}", addr);
axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service())
.await?;
Ok(())
}
使用流程(同 OAuth,但获取更多标准化用户信息)
/api/oidc/login?provider=google- 重定向到 Provider 登录
- 回调验证 ID Token + UserInfo
- 颁发自家 JWT + Refresh Cookie
参考资料
- openidconnect-rs GitHub: https://github.com/ramosbugs/openidconnect-rs
- openidconnect docs: https://docs.rs/openidconnect
- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
- Google OIDC Discovery: https://accounts.google.com/.well-known/openid-configuration
- Azure AD OIDC: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
现在你的 Axum 服务已支持完整标准化 OpenID Connect:
- 自动 Discovery + PKCE + Nonce
- ID Token 严格验证
- UserInfo 获取丰富 Claims
- 与自家 JWT 系统无缝融合
代码工业级、可扩展,支持任意 OIDC 兼容身份提供商(如 Keycloak、Auth0、Okta)。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)