Skip to main content

wx_rust_open/api/impl/
wx_open_o_auth2_service_impl.rs

1//! oauth2 网页授权服务实现(普通 appid/secret 链路)。
2//!
3//! 对应 Java `me.chanjar.weixin.open.api.impl.WxOpenOAuth2ServiceImpl`
4//! (`extends WxOpenServiceImpl implements WxOAuth2Service`,构造
5//! `new WxOpenOAuth2ServiceImpl(appId, appSecret, openConfigStorage)`)。
6//!
7//! Java 经 `this.get(url, null)`(`WxOpenServiceAbstractImpl` 裸 GET,
8//! 不做 token 注入)执行;Rust 以门面 `http_client` + 裸 GET 执行器
9//! 承载(ADAPTED:不经过门面 get 的 component_access_token 注入——OAuth2
10//! URL 已内嵌 appid/secret 或 access_token)。
11//!
12//! 与组件服务内联 oauth2([`crate::api::WxOpenComponentService`] 的
13//! `oauth2_get_access_token`/`oauth2_refresh_access_token`)的关系:
14//! Java 组件服务方法为 component 链路(`/sns/oauth2/component/access_token`,
15//! `@Deprecated`);本服务为普通链路(`/sns/oauth2/access_token`,
16//! appid/secret 直换),两者 URL 与语义均不同,Java 文档建议改用
17//! `getWxMpServiceByAppid(mpAppId).getOAuth2Service()`(即
18//! [`crate::api::r#impl::WxOpenMpOAuth2ServiceImpl`])。
19//!
20//! `validateAccessToken` 不在 Rust `WxOAuth2Service` trait 方法面内
21//! (trait 冻结时未收录),以固有方法承载(ADAPTED)。
22
23use std::sync::{Arc, Weak};
24
25use async_trait::async_trait;
26
27use wx_rust_common::bean::WxOAuth2UserInfo;
28use wx_rust_common::bean::oauth2::WxOAuth2AccessToken;
29use wx_rust_common::enums::WxType;
30use wx_rust_common::error::WxErrorException;
31use wx_rust_common::service::WxOAuth2Service;
32use wx_rust_common::util::http::{RequestExecutor, SimpleGetRequestExecutor};
33
34use crate::api::WxOpenService;
35use crate::enums::url_ma_domain::{
36    oauth2_access_token_url, oauth2_refresh_token_url, oauth2_userinfo_url,
37    oauth2_validate_token_url, qrconnect_url,
38};
39
40/// oauth2 网页授权服务实现(对应 Java `WxOpenOAuth2ServiceImpl`)。
41pub struct WxOpenOAuth2ServiceImpl {
42    /// 授权方 appid(Java 构造入参)。
43    app_id: String,
44    /// 授权方 appsecret(Java 构造入参)。
45    app_secret: String,
46    /// 门面服务弱引用(对应 Java 继承 `WxOpenServiceImpl` 持有的
47    /// `WxOpenConfigStorage`;Rust 以弱引用 + http_client 表达,ADAPTED)。
48    wx_open_service: Weak<dyn WxOpenService>,
49}
50
51impl WxOpenOAuth2ServiceImpl {
52    /// 构建服务(对应 Java
53    /// `new WxOpenOAuth2ServiceImpl(String appId, String appSecret,
54    /// WxOpenConfigStorage openConfigStorage)`)。
55    ///
56    /// # 参数
57    /// - `app_id`:授权方 appid
58    /// - `app_secret`:授权方 appsecret
59    /// - `wx_open_service`:门面服务强引用(内部降级为弱引用)
60    pub fn new(
61        app_id: String,
62        app_secret: String,
63        wx_open_service: Arc<dyn WxOpenService>,
64    ) -> Self {
65        Self {
66            app_id,
67            app_secret,
68            wx_open_service: Arc::downgrade(&wx_open_service),
69        }
70    }
71
72    /// 裸 GET(对应 Java `WxOpenServiceAbstractImpl.get(String, String)`:
73    /// 直调执行器,不做 token 注入;OAuth2 URL 已内嵌 appid/secret 或
74    /// access_token)。
75    async fn bare_get(&self, url: &str) -> Result<String, WxErrorException> {
76        let svc = self
77            .wx_open_service
78            .upgrade()
79            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))?;
80        let executor = SimpleGetRequestExecutor::new(svc.http_client().clone());
81        executor.execute(url, String::new(), WxType::Open).await
82    }
83
84    /// 验证 access token 是否有效(对应 Java
85    /// `validateAccessToken(WxOAuth2AccessToken token)`)。
86    ///
87    /// Java:GET `/sns/auth`,`WxErrorException` 捕获返回 false,
88    /// `IOException` 抛 `WxRuntimeException`;Rust 以 HTTP/解析错误抛
89    /// `-99`、业务错误返回 false 表达(ADAPTED:Rust 无
90    /// `WxRuntimeException`,统一以 `Err` 表达 IO 异常)。
91    pub async fn validate_access_token(
92        &self,
93        token: &WxOAuth2AccessToken,
94    ) -> Result<bool, WxErrorException> {
95        let svc = self
96            .wx_open_service
97            .upgrade()
98            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))?;
99        let config = svc.wx_open_config_storage();
100        let url = oauth2_validate_token_url(config.as_ref(), &token.access_token, &token.open_id);
101        match self.bare_get(&url).await {
102            Ok(_) => Ok(true),
103            Err(e) if e.error_code() != Some(-99) => Ok(false),
104            Err(e) => Err(e),
105        }
106    }
107}
108
109#[async_trait]
110impl WxOAuth2Service for WxOpenOAuth2ServiceImpl {
111    /// 构建网页授权 URL(对应 Java `buildAuthorizationUrl(...)`:
112    /// `QRCONNECT_URL` 格式化——redirect_uri 经 JS `encodeURIComponent`
113    /// 语义编码、state 经 trim)。
114    ///
115    /// 注意:Java 使用 `WxMpApiUrl.Other.QRCONNECT_URL`(网站应用授权
116    /// 登录链接 `/connect/qrconnect`),Rust 严格镜像。
117    fn build_authorization_url(&self, redirect_uri: &str, scope: &str, state: &str) -> String {
118        use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
119        // Java `URIUtil.encodeURIComponent(redirectUri)`:JS encodeURIComponent 语义
120        let encoded_redirect = utf8_percent_encode(redirect_uri, NON_ALPHANUMERIC).to_string();
121        // Java `StringUtils.trimToEmpty(state)`
122        let state = state.trim();
123        qrconnect_url(&self.app_id, &encoded_redirect, scope, state)
124    }
125
126    /// 用 code 获取 access token(对应 Java
127    /// `getAccessToken(String code)` → 委托
128    /// `getAccessToken(appId, appSecret, code)`)。
129    async fn get_access_token(&self, code: &str) -> Result<WxOAuth2AccessToken, WxErrorException> {
130        self.get_access_token_with(&self.app_id, &self.app_secret, code)
131            .await
132    }
133
134    /// 用 code 获取 access token(对应 Java
135    /// `getAccessToken(String appId, String appSecret, String code)`:
136    /// GET `/sns/oauth2/access_token?appid=&secret=&code=`)。
137    async fn get_access_token_with(
138        &self,
139        app_id: &str,
140        app_secret: &str,
141        code: &str,
142    ) -> Result<WxOAuth2AccessToken, WxErrorException> {
143        let svc = self
144            .wx_open_service
145            .upgrade()
146            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))?;
147        let config = svc.wx_open_config_storage();
148        let url = oauth2_access_token_url(config.as_ref(), app_id, app_secret, code);
149        let response = self.bare_get(&url).await?;
150        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
151    }
152
153    /// 刷新 access token(对应 Java
154    /// `refreshAccessToken(String refreshToken)`:GET
155    /// `/sns/oauth2/refresh_token?appid=&grant_type=refresh_token&refresh_token=`)。
156    async fn refresh_access_token(
157        &self,
158        refresh_token: &str,
159    ) -> Result<WxOAuth2AccessToken, WxErrorException> {
160        let svc = self
161            .wx_open_service
162            .upgrade()
163            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))?;
164        let config = svc.wx_open_config_storage();
165        let url = oauth2_refresh_token_url(config.as_ref(), &self.app_id, refresh_token);
166        let response = self.bare_get(&url).await?;
167        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
168    }
169
170    /// 获取用户信息(对应 Java
171    /// `getUserInfo(WxOAuth2AccessToken token, String lang)`:lang 为
172    /// null 时默认 `zh_CN`,GET `/sns/userinfo?access_token=&openid=&lang=`)。
173    async fn get_user_info(
174        &self,
175        token: &WxOAuth2AccessToken,
176        lang: &str,
177    ) -> Result<WxOAuth2UserInfo, WxErrorException> {
178        let lang = if lang.is_empty() { "zh_CN" } else { lang };
179        let svc = self
180            .wx_open_service
181            .upgrade()
182            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))?;
183        let config = svc.wx_open_config_storage();
184        let url = oauth2_userinfo_url(config.as_ref(), &token.access_token, &token.open_id, lang);
185        let response = self.bare_get(&url).await?;
186        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
187    }
188}