feat: gate recharge payment by login device
This commit is contained in:
@@ -66,6 +66,15 @@ pub enum BindingStatus {
|
||||
PendingBindPhone,
|
||||
}
|
||||
|
||||
// JWT 里只保留一份规范化后的设备快照,用于后端按登录设备拦截充值路径。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct AccessTokenDeviceInfo {
|
||||
pub client_type: String,
|
||||
pub client_runtime: String,
|
||||
pub client_platform: String,
|
||||
}
|
||||
|
||||
// 用于签发 access token 的领域输入,和最终 JWT claims 解耦,避免业务层手动拼 iat/exp/iss。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AccessTokenClaimsInput {
|
||||
@@ -92,6 +101,8 @@ pub struct AccessTokenClaims {
|
||||
pub binding_status: BindingStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub device: Option<AccessTokenDeviceInfo>,
|
||||
pub iat: u64,
|
||||
pub exp: u64,
|
||||
}
|
||||
@@ -1481,11 +1492,21 @@ impl AccessTokenClaims {
|
||||
input: AccessTokenClaimsInput,
|
||||
config: &JwtConfig,
|
||||
issued_at: OffsetDateTime,
|
||||
) -> Result<Self, JwtError> {
|
||||
Self::from_input_with_device(input, None, config, issued_at)
|
||||
}
|
||||
|
||||
pub fn from_input_with_device(
|
||||
input: AccessTokenClaimsInput,
|
||||
device: Option<AccessTokenDeviceInfo>,
|
||||
config: &JwtConfig,
|
||||
issued_at: OffsetDateTime,
|
||||
) -> Result<Self, JwtError> {
|
||||
let user_id = normalize_required_field(input.user_id, "JWT sub 不能为空")?;
|
||||
let session_id = normalize_required_field(input.session_id, "JWT sid 不能为空")?;
|
||||
let roles = normalize_roles(input.roles)?;
|
||||
let display_name = normalize_optional_field(input.display_name);
|
||||
let device = device.map(|device| device.normalize()).transpose()?;
|
||||
|
||||
let issued_at_unix = issued_at.unix_timestamp();
|
||||
if issued_at_unix < 0 {
|
||||
@@ -1515,6 +1536,7 @@ impl AccessTokenClaims {
|
||||
phone_verified: input.phone_verified,
|
||||
binding_status: input.binding_status,
|
||||
display_name,
|
||||
device,
|
||||
iat: issued_at_unix as u64,
|
||||
exp: expires_at_unix as u64,
|
||||
};
|
||||
@@ -1535,6 +1557,46 @@ impl AccessTokenClaims {
|
||||
self.ver
|
||||
}
|
||||
|
||||
pub fn client_type(&self) -> Option<&str> {
|
||||
self.device
|
||||
.as_ref()
|
||||
.map(|device| device.client_type.as_str())
|
||||
}
|
||||
|
||||
pub fn client_platform(&self) -> Option<&str> {
|
||||
self.device
|
||||
.as_ref()
|
||||
.map(|device| device.client_platform.as_str())
|
||||
}
|
||||
|
||||
pub fn is_wechat_mini_program_device(&self) -> bool {
|
||||
matches!(self.client_type(), Some("mini_program"))
|
||||
}
|
||||
|
||||
pub fn is_wechat_h5_device(&self) -> bool {
|
||||
matches!(self.client_type(), Some("wechat_h5"))
|
||||
}
|
||||
|
||||
pub fn is_wechat_payment_device(&self) -> bool {
|
||||
self.is_wechat_mini_program_device() || self.is_wechat_h5_device()
|
||||
}
|
||||
|
||||
pub fn is_mobile_device(&self) -> bool {
|
||||
matches!(self.client_platform(), Some("ios" | "android"))
|
||||
}
|
||||
|
||||
pub fn is_desktop_device(&self) -> bool {
|
||||
matches!(self.client_platform(), Some("windows" | "macos" | "linux"))
|
||||
}
|
||||
|
||||
pub fn is_mobile_wechat_browser_device(&self) -> bool {
|
||||
self.is_wechat_h5_device() && self.is_mobile_device()
|
||||
}
|
||||
|
||||
pub fn is_desktop_wechat_browser_device(&self) -> bool {
|
||||
self.is_wechat_h5_device() && self.is_desktop_device()
|
||||
}
|
||||
|
||||
pub fn validate_for_config(&self, config: &JwtConfig) -> Result<(), JwtError> {
|
||||
if self.iss.trim() != config.issuer() {
|
||||
return Err(JwtError::InvalidClaims("JWT iss 与当前配置不一致"));
|
||||
@@ -1543,6 +1605,9 @@ impl AccessTokenClaims {
|
||||
normalize_required_field(self.sub.clone(), "JWT sub 不能为空")?;
|
||||
normalize_required_field(self.sid.clone(), "JWT sid 不能为空")?;
|
||||
normalize_roles(self.roles.clone())?;
|
||||
if let Some(device) = &self.device {
|
||||
device.validate()?;
|
||||
}
|
||||
|
||||
if self.exp <= self.iat {
|
||||
return Err(JwtError::InvalidClaims("JWT exp 必须晚于 iat"));
|
||||
@@ -1552,6 +1617,38 @@ impl AccessTokenClaims {
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessTokenDeviceInfo {
|
||||
pub fn normalize(self) -> Result<Self, JwtError> {
|
||||
Ok(Self {
|
||||
client_type: normalize_required_field(
|
||||
self.client_type,
|
||||
"JWT device.client_type 不能为空",
|
||||
)?,
|
||||
client_runtime: normalize_required_field(
|
||||
self.client_runtime,
|
||||
"JWT device.client_runtime 不能为空",
|
||||
)?,
|
||||
client_platform: normalize_required_field(
|
||||
self.client_platform,
|
||||
"JWT device.client_platform 不能为空",
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), JwtError> {
|
||||
normalize_required_field(self.client_type.clone(), "JWT device.client_type 不能为空")?;
|
||||
normalize_required_field(
|
||||
self.client_runtime.clone(),
|
||||
"JWT device.client_runtime 不能为空",
|
||||
)?;
|
||||
normalize_required_field(
|
||||
self.client_platform.clone(),
|
||||
"JWT device.client_platform 不能为空",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_access_token(
|
||||
claims: &AccessTokenClaims,
|
||||
config: &JwtConfig,
|
||||
@@ -2129,10 +2226,7 @@ mod tests {
|
||||
let phone_info = payload.phone_info.expect("phone info should exist");
|
||||
|
||||
assert_eq!(phone_info.phone_number.as_deref(), Some("+8613800138000"));
|
||||
assert_eq!(
|
||||
phone_info.pure_phone_number.as_deref(),
|
||||
Some("13800138000")
|
||||
);
|
||||
assert_eq!(phone_info.pure_phone_number.as_deref(), Some("13800138000"));
|
||||
assert_eq!(phone_info.country_code.as_deref(), Some("86"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user