# wechat-backend-auth
[](https://crates.io/crates/wechat-backend-auth)
[](https://docs.rs/wechat-backend-auth)
[](https://github.com/zhenglongbing/wechat-backend-auth#license)
[](https://github.com/zhenglongbing/wechat-backend-auth/actions)
一个专为后端开发者设计的**完全无状态**的微信授权客户端,用于处理"前端传code,后端验证"的授权场景。
## 特点
- ✅ **完全无状态**: 不缓存token、不管理refresh_token、不管理会话
- ✅ **极简接口**: 仅封装微信API调用,没有复杂的Manager层
- ✅ **后端全权管理**: token存储、会话管理完全由业务代码决定
- ✅ **通用性**: 支持公众号、开放平台、移动应用的code验证
- ✅ **类型安全**: 使用newtype模式保护敏感信息
- ✅ **完善的错误处理**: 清晰的错误类型和自动重试机制
## 安装
在 `Cargo.toml` 中添加:
```toml
[dependencies]
wechat-backend-auth = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
## 快速开始
### 最简单的示例
```rust
use wechat_backend_auth::*;
#[tokio::main]
async fn main() -> Result<(), WeChatError> {
// 初始化客户端(注意:无需storage参数)
let client = BackendAuthClient::new(
BackendConfig::builder()
.app_id(AppId::new("wx1234567890abcdef"))
.app_secret(AppSecret::new("your_app_secret_here"))
.build()
)?;
// 前端传来的code
let code = "front_end_code_123";
// 换取token
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框架)
```rust
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> {
// 1. 换取token
let token_resp = state
.wechat_client
.exchange_code(AuthorizationCode::new(payload.code))
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 2. 获取用户信息
let user_info = state
.wechat_client
.get_user_info(&token_resp.access_token, &token_resp.openid)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 3. 创建JWT会话(你的业务逻辑)
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>> {
// 实现JWT生成逻辑
Ok(format!("jwt_token_for_{}", openid.as_str()))
}
```
### 场景2: 存储Token到Redis
```rust
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>> {
// 1. 换取token
let token_resp = wechat_client
.exchange_code(AuthorizationCode::new(code))
.await?;
// 2. 获取用户信息
let user_info = wechat_client
.get_user_info(&token_resp.access_token, &token_resp.openid)
.await?;
// 3. 存储access_token到Redis(2小时过期)
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?;
// 4. 存储refresh_token到Redis(30天过期)
conn.set_ex(
format!("wechat:refresh_token:{}", token_resp.openid.as_str()),
token_resp.refresh_token.expose_secret(),
30 * 24 * 3600, // 30天
)
.await?;
// 5. 创建业务会话
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刷新
```rust
use wechat_backend_auth::*;
async fn refresh_access_token(
client: &BackendAuthClient,
old_refresh_token: &str,
) -> Result<TokenResponse, WeChatError> {
// 使用refresh_token获取新的access_token
let new_token = client
.refresh_token(&RefreshToken::new(old_refresh_token))
.await?;
// 更新数据库或缓存
// db.update_token(&new_token.openid, &new_token).await?;
Ok(new_token)
}
```
### 场景4: Token验证
```rust
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
```rust
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`: 用户代理字符串
## 错误处理
```rust
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);
}
}
}
```
## 安全建议
1. **保护AppSecret**: 使用环境变量,不要硬编码
2. **使用HTTPS**: 生产环境必须使用HTTPS
3. **Token保护**: 库已使用`SecretString`包装敏感信息
4. **会话管理**: 使用JWT等成熟的会话管理方案
```rust
// 推荐: 从环境变量读取配置
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()
)?;
```
## 测试
```bash
# 运行所有测试
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:** 三种策略:
1. **主动刷新**(定时任务)
2. **被动刷新**(调用失败时)
3. **重新授权**(refresh_token也过期)
## License
MIT OR Apache-2.0
## 贡献
欢迎提交Issue和Pull Request!