添加微信登录相关接口

This commit is contained in:
李运家 2024-09-10 18:09:07 +08:00
parent e84d9c188c
commit 96a9bf72ac
9 changed files with 598 additions and 346 deletions

741
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -45,4 +45,7 @@ error-stack = "0.4"
jsonwebtoken = "9.3.0"
lazy_static = "1.4.0"
mimalloc = "0.1.42"
tokio-cron-scheduler = "0.10.2"
tokio-cron-scheduler = "0.10.2"
hmac = "0.12.1"
sha2 = "0.10.8"
hex-literal = "0.4.1"

View File

@ -16,4 +16,8 @@ options = { min_conns = 10, max_conns = 20, conn_timeout = 30, idle_timeout = 30
token_secret = "chuanyue"
refresh_token_secret = "chuanyue"
expires = 1800
refresh_expires = 3600
refresh_expires = 3600
[social.wechat]
app_id = "wx0d0b0b0b0b0b0b0b"
app_secret = "0d0b0b0b0d0b0b0b0d0b0b0b0b0b0b0b"

View File

@ -28,6 +28,9 @@ reqwest = { workspace = true, features = ["blocking", "json"] }
validator = { workspace = true }
moka = { workspace = true, features = ["future", "logging"] }
lazy_static = { workspace = true }
hmac = { workspace = true }
sha2 = { workspace = true }
hex-literal = { workspace = true }
domain = { path = "../domain" }
i18n = { path = "../i18n" }

View File

@ -8,7 +8,8 @@ pub struct Config {
pub server: Server,
pub logger: Logger,
pub database: Database,
pub jwt: Jwt
pub jwt: Jwt,
pub social: Social,
}
#[derive(Clone, Debug, Deserialize)]
@ -48,6 +49,20 @@ pub struct Jwt {
pub refresh_expires: i64,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Social {
pub wechat: Wechat,
// pub google: google::GoogleSocial,
// pub facebook: facebook::FacebookSocial,
// pub apple: apple::AppleSocial,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Wechat {
pub app_id: String,
pub app_secret: String,
}
#[macro_export]
macro_rules! config {
() => {

View File

@ -2,4 +2,5 @@ type SocialResult<T> = Result<T, Box<dyn std::error::Error>>;
pub mod google;
pub mod facebook;
pub mod apple;
pub mod apple;
pub mod wechat;

View File

@ -0,0 +1,166 @@
use std::sync::Arc;
use chrono::Utc;
use futures_util::lock::Mutex;
use hmac::{Hmac, Mac};
use lazy_static::lazy_static;
use serde::Deserialize;
use sha2::Sha256;
use crate::{
config,
res::response::ResErr,
};
use super::SocialResult;
lazy_static! {
pub static ref WECHAT_SOCIAL: WechatSocial = WechatSocial::default();
}
#[derive(Debug, Clone)]
pub struct WechatSocial {
app_id: String,
app_secret: String,
access_token: Arc<Mutex<WeChatAccessToken>>,
}
#[derive(Deserialize, Debug, Clone, Default)]
pub struct WeChatAccessToken {
pub access_token: String,
pub expires_in: i64,
}
#[derive(Deserialize, Debug, Clone)]
pub struct WeChatBaseResult {
// 错误码
pub errcode: i64,
// 错误信息
pub errmsg: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct MiniAppLoginResult {
// 用户唯一标识
pub openid: String,
// 会话密钥
pub session_key: String,
// 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回
// 如果开发者拥有多个移动应用、网站应用、和公众账号(包括小程序),可通过 UnionID 来区分用户的唯一性
// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
pub unionid: String,
// 错误码
pub errcode: i64,
// 错误信息
pub errmsg: String,
}
impl Default for WechatSocial {
fn default() -> Self {
let wechat_option = &config!().social.wechat;
Self {
app_id: wechat_option.app_id.to_owned(),
app_secret: wechat_option.app_secret.to_owned(),
access_token: Arc::new(Mutex::new(WeChatAccessToken {
access_token: "".to_string(),
expires_in: 0,
})),
}
}
}
impl WechatSocial {
/// 异步获取微信公钥,适用于小程序和小游戏
/// 本接口和getStableAccessToken类似
/// 区别为:
/// getAccessToken每次调用都会生成新的access_token
/// getStableAccessToken在force_refresh=false模式下会尽量返回相同的access_token。
/// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
///
/// https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/access-token/auth.getAccessToken.html
/// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
async fn fetch_and_parse_wechat_access_token(&self) -> SocialResult<WeChatAccessToken> {
let mut wechat_access_token = self.access_token.lock().await;
if wechat_access_token.access_token.is_empty()
&& wechat_access_token.expires_in < Utc::now().timestamp()
{
let response = reqwest::get(format!(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}",
self.app_id,
self.app_secret
))
.await
.unwrap();
let mut new_wechat_access_token: WeChatAccessToken = response.json().await.unwrap();
tracing::info!("wechat access token: {:?}", new_wechat_access_token);
new_wechat_access_token.expires_in =
Utc::now().timestamp() + wechat_access_token.expires_in;
*wechat_access_token = new_wechat_access_token;
}
tracing::info!("wechat access token: {:?}", wechat_access_token);
Ok(wechat_access_token.clone())
}
/// 微信登录登录
/// 本接口适用于小程序和小游戏
/// https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/login/auth.code2Session.html
/// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
pub async fn code_2_session(&self, code: &str) -> SocialResult<MiniAppLoginResult> {
let response = reqwest::get(format!(
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
self.app_id,
self.app_secret,
code
))
.await
.unwrap();
let result: MiniAppLoginResult = response.json().await.unwrap();
if result.errcode != 0 {
tracing::error!(
"微信登录失败,errcode:{},errmsg:{}",
result.errcode,
result.errmsg
);
return Err(Box::new(ResErr::social(format!(
"微信登录失败,errcode:{},errmsg:{}",
result.errcode, result.errmsg
))));
}
Ok(result)
}
/// 验证登录信息
/// 本接口适用于小程序和小游戏
///
/// https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/login/auth.checkSessionKey.html
/// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/checkSessionKey.html
/// todo 使用的hmac签名是否正确签名结果转换成字符串是否正确
pub async fn check_session(&self, session_key: &str, open_id: &str) -> SocialResult<()> {
// let signature = Hmac::<Sha256>::new_from_slice(session.as_bytes()).expect("");
let mut mac =
Hmac::<Sha256>::new_from_slice(session_key.as_bytes()).expect("初始化hmac失败");
mac.update(b"");
let out = mac.finalize().into_bytes();
let signature = std::str::from_utf8(out.trim_ascii())
.unwrap_or_default()
.trim_ascii()
.to_string();
let response = reqwest::get(format!(
"https://api.weixin.qq.com/wxa/checksession?access_token={}&signature={}&openid={}&sig_method=hmac_sha256",
self.access_token.lock().await.access_token,
signature,
open_id
))
.await
.unwrap();
let result: WeChatBaseResult = response.json().await.unwrap();
if result.errcode != 0 {
tracing::error!("微信登录校验失败,errcode:{},errmsg:{}", result.errcode, result.errmsg);
return Err(Box::new(ResErr::social(format!(
"微信登录校验失败,errcode:{},errmsg:{}",
result.errcode, result.errmsg
))));
}
Ok(())
}
}

2
service/src/social.rs Normal file
View File

@ -0,0 +1,2 @@
/// 用于接收各个服务平台的回调、消息推送

View File

@ -14,6 +14,7 @@ use library::{
cache::account_cache::{CacheAccount, LOGIN_CACHE}, context::WhiteContext, db, res::response::{ResData, ResErr, ResResult}, token::{generate_refresh_token, generate_token}
};
/// 登录, 使用账号和密码
pub async fn authenticate_with_password(
context: WhiteContext,
req: AuthenticateWithPassword,