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}