wechat_backend_auth/
client.rs

1use crate::config::BackendConfig;
2use crate::error::WeChatError;
3use crate::transport::HttpTransport;
4use crate::types::{
5    AccessToken, AuthorizationCode, OpenId, RawTokenResponse, RefreshToken, TokenResponse, UserInfo,
6};
7use std::sync::Arc;
8
9/// 后端授权客户端 - 完全无状态设计
10///
11/// 专为后端API开发者设计的微信授权客户端,处理"前端传code,后端验证"的场景。
12///
13/// # 特点
14///
15/// - ✅ **完全无状态**:不缓存token、不管理refresh_token、不管理会话
16/// - ✅ **极简接口**:仅封装微信API调用,没有复杂的Manager层
17/// - ✅ **后端全权管理**:token存储、会话管理完全由业务代码决定
18/// - ✅ **通用性**:支持公众号、开放平台、移动应用的code验证
19///
20/// # 示例
21///
22/// ```no_run
23/// use wechat_backend_auth::*;
24///
25/// # async fn example() -> Result<(), WeChatError> {
26/// let client = BackendAuthClient::new(
27///     BackendConfig::builder()
28///         .app_id(AppId::new("wx1234567890abcdef"))
29///         .app_secret(AppSecret::new("your_app_secret_here"))
30///         .build()
31/// )?;
32///
33/// // 换取token
34/// let token_resp = client
35///     .exchange_code(AuthorizationCode::new("code_from_frontend"))
36///     .await?;
37///
38/// // 获取用户信息
39/// let user_info = client
40///     .get_user_info(&token_resp.access_token, &token_resp.openid)
41///     .await?;
42///
43/// println!("用户登录: {} ({})", user_info.nickname, user_info.openid);
44/// # Ok(())
45/// # }
46/// ```
47pub struct BackendAuthClient {
48    config: Arc<BackendConfig>,
49    transport: Arc<HttpTransport>,
50}
51
52impl BackendAuthClient {
53    /// 创建新的后端授权客户端
54    ///
55    /// # 参数
56    ///
57    /// - `config`: 后端授权配置
58    ///
59    /// # 返回
60    ///
61    /// 返回配置好的客户端实例或配置错误
62    ///
63    /// # 示例
64    ///
65    /// ```no_run
66    /// use wechat_backend_auth::*;
67    ///
68    /// # fn example() -> Result<(), WeChatError> {
69    /// let client = BackendAuthClient::new(
70    ///     BackendConfig::builder()
71    ///         .app_id(AppId::new("wxAppId"))
72    ///         .app_secret(AppSecret::new("secret"))
73    ///         .build()
74    /// )?;
75    /// # Ok(())
76    /// # }
77    /// ```
78    pub fn new(config: BackendConfig) -> Result<Self, WeChatError> {
79        let transport = HttpTransport::new(&config.http)?;
80        let config = Arc::new(config);
81        let transport = Arc::new(transport);
82
83        Ok(Self { config, transport })
84    }
85
86    /// 使用授权码换取访问令牌
87    ///
88    /// 调用微信 `oauth2/access_token` 接口,使用前端传来的code换取:
89    /// - `access_token`: 访问令牌(有效期2小时)
90    /// - `refresh_token`: 刷新令牌(有效期30天)
91    /// - `openid`: 用户唯一标识
92    /// - `unionid`: 开放平台统一标识(可选)
93    ///
94    /// # 参数
95    ///
96    /// - `code`: 前端通过微信授权获取的临时授权码(有效期5分钟,只能使用一次)
97    ///
98    /// # 返回
99    ///
100    /// 返回 `TokenResponse`,包含token和用户标识,**不会自动存储**
101    ///
102    /// # 后续处理
103    ///
104    /// 后端开发者需要自行决定token的处理方式:
105    /// - 存入数据库/Redis(如需长期使用)
106    /// - 直接返回给前端(让前端管理)
107    /// - 立即调用 `get_user_info()` 后丢弃
108    ///
109    /// # 示例
110    ///
111    /// ```no_run
112    /// # use wechat_backend_auth::*;
113    /// # async fn example(client: &BackendAuthClient, code: String) -> Result<(), WeChatError> {
114    /// let token_resp = client
115    ///     .exchange_code(AuthorizationCode::new(code))
116    ///     .await?;
117    ///
118    /// // 后端决定token的去向
119    /// // redis.set(
120    /// //     format!("wechat:token:{}", token_resp.openid),
121    /// //     &token_resp,
122    /// //     token_resp.expires_in
123    /// // ).await?;
124    /// # Ok(())
125    /// # }
126    /// ```
127    pub async fn exchange_code(
128        &self,
129        code: AuthorizationCode,
130    ) -> Result<TokenResponse, WeChatError> {
131        let url = format!(
132            "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code",
133            self.config.app_id().as_str(),
134            self.config.app_secret().expose_secret(),
135            code.expose_secret()
136        );
137
138        let raw: RawTokenResponse = self.transport.get(&url).await?;
139        Ok(TokenResponse::from(raw))
140    }
141
142    /// 获取用户详细信息
143    ///
144    /// 使用 `access_token` 调用微信 `sns/userinfo` 接口获取用户资料。
145    ///
146    /// # 参数
147    ///
148    /// - `access_token`: 访问令牌(来自 `exchange_code()` 或存储的历史token)
149    /// - `openid`: 用户唯一标识
150    ///
151    /// # 返回
152    ///
153    /// 返回 `UserInfo`,包含:
154    /// - `openid`: 用户标识
155    /// - `nickname`: 昵称
156    /// - `headimgurl`: 头像URL
157    /// - `sex`: 性别
158    /// - `province/city/country`: 地域信息
159    /// - `unionid`: 开放平台统一标识(可选)
160    ///
161    /// # 注意
162    ///
163    /// - 需要用户授权了 `snsapi_userinfo` 作用域
164    /// - 如果只授权了 `snsapi_base`,只能获取 `openid`
165    /// - token可能已过期,需要处理 `AccessTokenExpired` 错误
166    ///
167    /// # 示例
168    ///
169    /// ```no_run
170    /// # use wechat_backend_auth::*;
171    /// # async fn example(client: &BackendAuthClient, token_resp: &TokenResponse) -> Result<(), WeChatError> {
172    /// let user_info = client
173    ///     .get_user_info(&token_resp.access_token, &token_resp.openid)
174    ///     .await?;
175    ///
176    /// println!("用户昵称: {}", user_info.nickname);
177    /// # Ok(())
178    /// # }
179    /// ```
180    pub async fn get_user_info(
181        &self,
182        access_token: &AccessToken,
183        openid: &OpenId,
184    ) -> Result<UserInfo, WeChatError> {
185        let url = format!(
186            "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}&lang=zh_CN",
187            access_token.expose_secret(),
188            openid.as_str()
189        );
190
191        self.transport.get(&url).await
192    }
193
194    /// 验证访问令牌是否有效
195    ///
196    /// 调用微信 `sns/auth` 接口检查 `access_token` 是否仍然有效。
197    ///
198    /// # 参数
199    ///
200    /// - `access_token`: 要验证的访问令牌
201    /// - `openid`: 用户唯一标识
202    ///
203    /// # 返回
204    ///
205    /// - `Ok(true)`: token有效
206    /// - `Ok(false)`: token无效或已过期
207    ///
208    /// # 使用场景
209    ///
210    /// - 前端传来token,验证其有效性
211    /// - 使用缓存的历史token前先验证
212    /// - 决定是否需要调用 `refresh_token()`
213    ///
214    /// # 示例
215    ///
216    /// ```no_run
217    /// # use wechat_backend_auth::*;
218    /// # async fn example(client: &BackendAuthClient, cached_token: &AccessToken, openid: &OpenId) -> Result<(), WeChatError> {
219    /// if !client.validate_token(cached_token, openid).await? {
220    ///     // token已过期,需要刷新或重新授权
221    ///     // let new_token = client.refresh_token(&refresh_token).await?;
222    /// }
223    /// # Ok(())
224    /// # }
225    /// ```
226    pub async fn validate_token(
227        &self,
228        access_token: &AccessToken,
229        openid: &OpenId,
230    ) -> Result<bool, WeChatError> {
231        let url = format!(
232            "https://api.weixin.qq.com/sns/auth?access_token={}&openid={}",
233            access_token.expose_secret(),
234            openid.as_str()
235        );
236
237        #[derive(serde::Deserialize)]
238        struct ValidateResponse {
239            errcode: i32,
240        }
241
242        match self.transport.get::<ValidateResponse>(&url).await {
243            Ok(resp) => Ok(resp.errcode == 0),
244            Err(WeChatError::AccessTokenExpired { .. }) => Ok(false),
245            Err(e) => Err(e),
246        }
247    }
248
249    /// 刷新访问令牌
250    ///
251    /// 使用 `refresh_token` 获取新的 `access_token`。
252    /// `refresh_token` 有效期为30天,比 `access_token`(2小时)长得多。
253    ///
254    /// # 参数
255    ///
256    /// - `refresh_token`: 刷新令牌(从 `exchange_code()` 响应中获取)
257    ///
258    /// # 返回
259    ///
260    /// 返回新的 `TokenResponse`,**不会自动存储**,需手动更新缓存
261    ///
262    /// # 注意
263    ///
264    /// - `refresh_token` 本身也会更新,需要同时保存新的 `refresh_token`
265    /// - 如果 `refresh_token` 也过期,需要重新引导用户授权
266    ///
267    /// # 示例
268    ///
269    /// ```no_run
270    /// # use wechat_backend_auth::*;
271    /// # async fn example(client: &BackendAuthClient, old_refresh_token: &RefreshToken) -> Result<(), WeChatError> {
272    /// // 刷新获取新token
273    /// let new_token = client
274    ///     .refresh_token(old_refresh_token)
275    ///     .await?;
276    ///
277    /// // 更新数据库
278    /// // db.update_token(&openid, &new_token).await?;
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub async fn refresh_token(
283        &self,
284        refresh_token: &RefreshToken,
285    ) -> Result<TokenResponse, WeChatError> {
286        let url = format!(
287            "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={}&grant_type=refresh_token&refresh_token={}",
288            self.config.app_id().as_str(),
289            refresh_token.expose_secret()
290        );
291
292        let raw: RawTokenResponse = self.transport.get(&url).await?;
293        Ok(TokenResponse::from(raw))
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::types::{AppId, AppSecret};
301
302    #[test]
303    fn test_client_creation() {
304        let config = BackendConfig::builder()
305            .app_id(AppId::new("wx_test"))
306            .app_secret(AppSecret::new("test_secret"))
307            .build();
308
309        let client = BackendAuthClient::new(config);
310        assert!(client.is_ok());
311    }
312}