wechat-backend-auth 0.1.0

A stateless WeChat OAuth client for backend API developers
Documentation
# wechat-backend-auth

[![Crates.io](https://img.shields.io/crates/v/wechat-backend-auth.svg)](https://crates.io/crates/wechat-backend-auth)
[![Documentation](https://docs.rs/wechat-backend-auth/badge.svg)](https://docs.rs/wechat-backend-auth)
[![License](https://img.shields.io/crates/l/wechat-backend-auth.svg)](https://github.com/zhenglongbing/wechat-backend-auth#license)
[![Build Status](https://img.shields.io/github/actions/workflow/status/zhenglongbing/wechat-backend-auth/ci.yml?branch=master)](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!