wechat_minapp/
credential.rs

1use std::collections::HashMap;
2
3use aes::{
4    Aes128,
5    cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7,generic_array::GenericArray},
6};
7use base64::{Engine, engine::general_purpose::STANDARD};
8use cbc::Decryptor;
9use hex::encode;
10use hmac::{Hmac, Mac};
11use serde::{Deserialize, Serialize};
12use serde_json::from_slice;
13use sha2::Sha256;
14use tracing::{Level, event, instrument};
15
16use crate::{
17    Result,
18    client::Client,
19    constants,
20    error::Error::InternalServer,
21    response::Response,
22    user::{User, UserBuilder},
23};
24
25type Aes128CbcDec = Decryptor<Aes128>;
26
27#[derive(Serialize, Deserialize, Clone)]
28pub struct Credential {
29    open_id: String,
30    session_key: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    union_id: Option<String>,
33}
34
35impl Credential {
36    pub fn open_id(&self) -> &str {
37        &self.open_id
38    }
39
40    pub fn session_key(&self) -> &str {
41        &self.session_key
42    }
43
44    pub fn union_id(&self) -> Option<&str> {
45        self.union_id.as_deref()
46    }
47
48    /// 解密用户数据,使用的是 AES-128-CBC 算法,数据采用PKCS#7填充。
49    /// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
50    /// ```rust
51    /// use axum::{extract::State, response::IntoResponse, Json};
52    /// use wechat_minapp::{Client, Result};
53    /// use serde::Deserialize;
54    ///
55    /// #[derive(Deserialize, Default)]
56    /// pub(crate) struct EncryptedPayload {
57    ///     code: String,
58    ///     encrypted_data: String,
59    ///     iv: String,
60    /// }
61    ///
62    /// pub(crate) async fn decrypt(
63    ///     State(client): State<Client>,
64    ///     Json(payload): Json<EncryptedPayload>,
65    /// ) -> Result<impl IntoResponse> {
66    ///     let credential = client.login(&payload.code).await?;
67    ///
68    ///     let user = credential.decrypt(&payload.encrypted_data, &payload.iv)?;
69    ///
70    ///     Ok(())
71    /// }
72    /// ```
73    #[instrument(skip(self, encrypted_data, iv))]
74    pub fn decrypt(&self, encrypted_data: &str, iv: &str) -> Result<User> {
75        event!(Level::DEBUG, "encrypted_data: {}", encrypted_data);
76        event!(Level::DEBUG, "iv: {}", iv);
77
78        let key = STANDARD.decode(self.session_key.as_bytes())?;
79        let iv = STANDARD.decode(iv.as_bytes())?;
80
81        let decryptor = Aes128CbcDec::new(
82            &GenericArray::clone_from_slice(&key),
83            &GenericArray::clone_from_slice(&iv),
84        );
85
86        let encrypted_data = STANDARD.decode(encrypted_data.as_bytes())?;
87
88        let buffer = decryptor.decrypt_padded_vec_mut::<Pkcs7>(&encrypted_data)?;
89
90        let builder = from_slice::<UserBuilder>(&buffer)?;
91
92        event!(Level::DEBUG, "user builder: {:#?}", builder);
93
94        Ok(builder.build())
95    }
96}
97
98impl std::fmt::Debug for Credential {
99    // 为了安全,不打印 session_key
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("Credential")
102            .field("open_id", &self.open_id)
103            .field("session_key", &"********")
104            .field("union_id", &self.union_id)
105            .finish()
106    }
107}
108
109#[derive(Deserialize)]
110pub(crate) struct CredentialBuilder {
111    #[serde(rename = "openid")]
112    open_id: String,
113    session_key: String,
114    #[serde(rename = "unionid")]
115    union_id: Option<String>,
116}
117
118impl CredentialBuilder {
119    pub(crate) fn build(self) -> Credential {
120        Credential {
121            open_id: self.open_id,
122            session_key: self.session_key,
123            union_id: self.union_id,
124        }
125    }
126}
127
128impl std::fmt::Debug for CredentialBuilder {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("CredentialBuilder")
131            .field("open_id", &self.open_id)
132            .field("session_key", &"********")
133            .field("union_id", &self.union_id)
134            .finish()
135    }
136}
137
138type HmacSha256 = Hmac<Sha256>;
139
140impl Client {
141    /// 检查登录态是否过期
142    /// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/checkSessionKey.html
143    #[instrument(skip(self, session_key, open_id))]
144    pub async fn check_session_key(&self, session_key: &str, open_id: &str) -> Result<()> {
145        let mut mac = HmacSha256::new_from_slice(session_key.as_bytes())?;
146        mac.update(b"");
147        let hasher = mac.finalize();
148        let signature = encode(hasher.into_bytes());
149
150        let mut map = HashMap::new();
151
152        map.insert("openid", open_id.to_string());
153        map.insert("signature", signature);
154        map.insert("sig_method", "hmac_sha256".into());
155
156        let response = self
157            .request()
158            .get(constants::CHECK_SESSION_KEY_END_POINT)
159            .query(&map)
160            .send()
161            .await?;
162
163        event!(Level::DEBUG, "response: {:#?}", response);
164
165        if response.status().is_success() {
166            let response = response.json::<Response<()>>().await?;
167
168            response.extract()
169        } else {
170            Err(crate::error::Error::InternalServer(response.text().await?))
171        }
172    }
173
174    /// 重置用户的 session_key
175    /// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/ResetUserSessionKey.html
176    #[instrument(skip(self, open_id))]
177    pub async fn reset_session_key(&self, session_key: &str, open_id: &str) -> Result<Credential> {
178        let mut mac = HmacSha256::new_from_slice(session_key.as_bytes())?;
179        mac.update(b"");
180        let hasher = mac.finalize();
181        let signature = encode(hasher.into_bytes());
182
183        let mut map = HashMap::new();
184
185        map.insert("access_token", self.access_token().await?);
186        map.insert("openid", open_id.to_string());
187        map.insert("signature", signature);
188        map.insert("sig_method", "hmac_sha256".into());
189
190        let response = self
191            .request()
192            .get(constants::RESET_SESSION_KEY_END_POINT)
193            .query(&map)
194            .send()
195            .await?;
196
197        event!(Level::DEBUG, "response: {:#?}", response);
198
199        if response.status().is_success() {
200            let response = response.json::<Response<CredentialBuilder>>().await?;
201
202            let credential = response.extract()?.build();
203
204            event!(Level::DEBUG, "credential: {:#?}", credential);
205
206            Ok(credential)
207        } else {
208            Err(InternalServer(response.text().await?))
209        }
210    }
211}