wechat-backend-auth

一个专为后端开发者设计的完全无状态的微信授权客户端,用于处理"前端传code,后端验证"的授权场景。
特点
- ✅ 完全无状态: 不缓存token、不管理refresh_token、不管理会话
- ✅ 极简接口: 仅封装微信API调用,没有复杂的Manager层
- ✅ 后端全权管理: token存储、会话管理完全由业务代码决定
- ✅ 通用性: 支持公众号、开放平台、移动应用的code验证
- ✅ 类型安全: 使用newtype模式保护敏感信息
- ✅ 完善的错误处理: 清晰的错误类型和自动重试机制
安装
在 Cargo.toml 中添加:
[dependencies]
wechat-backend-auth = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
快速开始
最简单的示例
use wechat_backend_auth::*;
#[tokio::main]
async fn main() -> Result<(), WeChatError> {
let client = BackendAuthClient::new(
BackendConfig::builder()
.app_id(AppId::new("wx1234567890abcdef"))
.app_secret(AppSecret::new("your_app_secret_here"))
.build()
)?;
let code = "front_end_code_123";
let token_resp = client
.exchange_code(AuthorizationCode::new(code))
.await?;
let user_info = client
.get_user_info(&token_resp.access_token, &token_resp.openid)
.await?;
println!("用户登录: {} ({})", user_info.nickname, user_info.openid);
println!("Token有效期: {:?}", token_resp.expires_in);
Ok(())
}
使用场景
场景1: RESTful API (Axum框架)
use axum::{
extract::{Json, State},
http::StatusCode,
response::IntoResponse,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use wechat_backend_auth::*;
#[derive(Deserialize)]
struct LoginRequest {
code: String,
}
#[derive(Serialize)]
struct LoginResponse {
token: String,
user: UserData,
}
#[derive(Serialize)]
struct UserData {
openid: String,
nickname: String,
avatar: String,
}
struct AppState {
wechat_client: BackendAuthClient,
}
async fn login_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
let token_resp = state
.wechat_client
.exchange_code(AuthorizationCode::new(payload.code))
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
let user_info = state
.wechat_client
.get_user_info(&token_resp.access_token, &token_resp.openid)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
let jwt_token = create_jwt_token(&user_info.openid)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(LoginResponse {
token: jwt_token,
user: UserData {
openid: user_info.openid.as_str().to_string(),
nickname: user_info.nickname,
avatar: user_info.headimgurl,
},
}))
}
#[tokio::main]
async fn main() {
let wechat_client = BackendAuthClient::new(
BackendConfig::builder()
.app_id(AppId::new("wx_app_id"))
.app_secret(AppSecret::new("secret"))
.build()
).unwrap();
let state = Arc::new(AppState { wechat_client });
let app = Router::new()
.route("/api/auth/login", post(login_handler))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn create_jwt_token(openid: &OpenId) -> Result<String, Box<dyn std::error::Error>> {
Ok(format!("jwt_token_for_{}", openid.as_str()))
}
场景2: 存储Token到Redis
use redis::AsyncCommands;
use wechat_backend_auth::*;
async fn login_with_redis(
wechat_client: &BackendAuthClient,
redis_client: &redis::Client,
code: String,
) -> Result<String, Box<dyn std::error::Error>> {
let token_resp = wechat_client
.exchange_code(AuthorizationCode::new(code))
.await?;
let user_info = wechat_client
.get_user_info(&token_resp.access_token, &token_resp.openid)
.await?;
let mut conn = redis_client.get_async_connection().await?;
conn.set_ex(
format!("wechat:access_token:{}", token_resp.openid.as_str()),
token_resp.access_token.expose_secret(),
token_resp.expires_in.as_secs() as usize,
)
.await?;
conn.set_ex(
format!("wechat:refresh_token:{}", token_resp.openid.as_str()),
token_resp.refresh_token.expose_secret(),
30 * 24 * 3600, )
.await?;
let session_token = create_session(&user_info.openid)?;
Ok(session_token)
}
fn create_session(openid: &OpenId) -> Result<String, Box<dyn std::error::Error>> {
Ok(format!("session_{}", openid.as_str()))
}
场景3: Token刷新
use wechat_backend_auth::*;
async fn refresh_access_token(
client: &BackendAuthClient,
old_refresh_token: &str,
) -> Result<TokenResponse, WeChatError> {
let new_token = client
.refresh_token(&RefreshToken::new(old_refresh_token))
.await?;
Ok(new_token)
}
场景4: Token验证
use wechat_backend_auth::*;
async fn validate_user_token(
client: &BackendAuthClient,
access_token: String,
openid: String,
) -> Result<bool, WeChatError> {
let is_valid = client
.validate_token(
&AccessToken::new(access_token),
&OpenId::new(openid),
)
.await?;
if is_valid {
println!("✅ Token有效");
} else {
println!("❌ Token无效,需要刷新或重新授权");
}
Ok(is_valid)
}
API文档
BackendAuthClient
主要方法:
new(config: BackendConfig) -> Result<Self, WeChatError>
创建新的后端授权客户端。
exchange_code(code: AuthorizationCode) -> Result<TokenResponse, WeChatError>
使用授权码换取访问令牌。
get_user_info(access_token: &AccessToken, openid: &OpenId) -> Result<UserInfo, WeChatError>
获取用户详细信息。
validate_token(access_token: &AccessToken, openid: &OpenId) -> Result<bool, WeChatError>
验证访问令牌是否有效。
refresh_token(refresh_token: &RefreshToken) -> Result<TokenResponse, WeChatError>
刷新访问令牌。
配置选项
BackendConfig
let config = BackendConfig::builder()
.app_id(AppId::new("wx_app_id"))
.app_secret(AppSecret::new("secret"))
.http(
HttpConfig::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.max_retries(3)
.build()
)
.build();
HttpConfig选项
timeout: 请求超时时间(默认30秒)
connect_timeout: 连接超时时间(默认10秒)
max_retries: 最大重试次数(默认3次)
retry_delay: 重试延迟(默认1秒)
user_agent: 用户代理字符串
错误处理
use wechat_backend_auth::*;
async fn handle_errors(client: &BackendAuthClient, code: String) {
match client.exchange_code(AuthorizationCode::new(code)).await {
Ok(token) => {
println!("成功: {:?}", token);
}
Err(WeChatError::InvalidCode { code, msg }) => {
eprintln!("无效的授权码 ({}): {}", code, msg);
}
Err(WeChatError::CodeUsed) => {
eprintln!("授权码已被使用");
}
Err(WeChatError::AccessTokenExpired { code }) => {
eprintln!("Token已过期: {}", code);
}
Err(WeChatError::Transport(e)) => {
eprintln!("网络错误: {}", e);
}
Err(e) => {
eprintln!("其他错误: {}", e);
}
}
}
安全建议
- 保护AppSecret: 使用环境变量,不要硬编码
- 使用HTTPS: 生产环境必须使用HTTPS
- Token保护: 库已使用
SecretString包装敏感信息
- 会话管理: 使用JWT等成熟的会话管理方案
let client = BackendAuthClient::new(
BackendConfig::builder()
.app_id(AppId::new(std::env::var("WECHAT_APP_ID")?))
.app_secret(AppSecret::new(std::env::var("WECHAT_APP_SECRET")?))
.build()
)?;
测试
cargo test
export WECHAT_APP_ID="your_app_id"
export WECHAT_APP_SECRET="your_app_secret"
export WECHAT_TEST_CODE="valid_code_from_wechat"
cargo test --test integration_test -- --ignored
常见问题
Q: 为什么去掉TokenManager?
A: 后端场景下,token的管理策略因业务而异。自动管理反而限制了灵活性,不如交给开发者自行决定。
Q: token应该存哪里?
A: 取决于业务需求:
- Redis: 快速、支持TTL - 适合高并发API
- 数据库: 持久化、易查询 - 适合需要长期存储
- 前端存储: 后端无状态 - 适合简单应用
- 不存储: 最简单 - 适合一次性验证
推荐: Redis(access_token) + 数据库(refresh_token)
Q: 如何处理token过期?
A: 三种策略:
- 主动刷新(定时任务)
- 被动刷新(调用失败时)
- 重新授权(refresh_token也过期)
License
MIT OR Apache-2.0
贡献
欢迎提交Issue和Pull Request!