feat: update jwt encode and decode

This commit is contained in:
xfy
2024-11-20 11:53:01 +08:00
parent dcd66933aa
commit f77657a0ce
5 changed files with 249 additions and 33 deletions

View File

@ -41,6 +41,21 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.93"
@ -94,9 +109,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.7.7"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
@ -147,6 +162,30 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
dependencies = [
"axum",
"axum-core",
"bytes",
"fastrand",
"futures-util",
"headers",
"http",
"http-body",
"http-body-util",
"mime",
"multer",
"pin-project-lite",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.74"
@ -260,6 +299,26 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.14"
@ -360,6 +419,21 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "fastrand"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "flate2"
version = "1.0.34"
@ -454,6 +528,30 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64 0.21.7",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -547,6 +645,29 @@ dependencies = [
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -833,6 +954,23 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -955,6 +1093,8 @@ dependencies = [
"anyhow",
"argon2",
"axum",
"axum-extra",
"chrono",
"dotenvy",
"jsonwebtoken",
"rand",
@ -1231,6 +1371,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1785,6 +1936,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
# server
axum = "0.7.7"
axum-extra = { version = "0.9.6", features = ["typed-header"] }
tokio = { version = "1.41.1", features = ["full"] }
tower = "0.5.1"
tower-http = { version = "0.6.1", features = ["full"] }
@ -24,6 +25,7 @@ serde_json = { version = "1.0.132" }
serde_repr = "0.1.19"
jsonwebtoken = "9.3.0"
validator = { version = "0.19.0", features = ["derive"] }
chrono = "0.4.38"
[profile.release]
lto = true

View File

@ -1,4 +1,4 @@
use std::fmt::Display;
use std::{borrow::Cow, fmt::Display};
use axum::{
extract::rejection::{FormRejection, JsonRejection},
@ -22,12 +22,13 @@ pub enum AppError {
AxumFormRejection(#[from] FormRejection),
#[error(transparent)]
AxumJsonRejection(#[from] JsonRejection),
// jwt
#[error(transparent)]
Jwt(#[from] jsonwebtoken::errors::Error),
// route
// 路由通常错误 错误信息直接返回用户
// #[error("{0}")]
// AuthorizeFailed(Cow<'static, str>),
// #[error("{0}")]
// UserConflict(Cow<'static, str>),
#[error("{0}")]
InvalidToken(Cow<'static, str>),
}
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
@ -35,7 +36,7 @@ pub enum AppError {
pub enum ErrorCode {
Normal = 200,
InternalError = 1000,
//NotAuthorized = 1001,
// NotAuthorized = 1001,
AuthorizeFailed = 1002,
UserConflict = 1003,
ParameterIncorrect = 1004,
@ -77,6 +78,7 @@ impl IntoResponse for AppError {
let (status_code, code, err_message) = match self {
AppError::Any(err) => log_internal_error(err),
AppError::Jwt(err) => log_internal_error(err),
AppError::AxumFormRejection(_) | AppError::AxumJsonRejection(_) => (
StatusCode::BAD_REQUEST,
ParameterIncorrect,
@ -85,11 +87,12 @@ impl IntoResponse for AppError {
AppError::ValidationError(_) => {
let message = format!("Input validation error: [{self}]").replace('\n', ", ");
(StatusCode::BAD_REQUEST, ParameterIncorrect, message)
} // route
// AppError::AuthorizeFailed(err) => {
// (StatusCode::UNAUTHORIZED, AuthorizeFailed, err.to_string())
// }
// AppError::UserConflict(err) => (StatusCode::CONFLICT, UserConflict, err.to_string()),
}
AppError::InvalidToken(_) => (
StatusCode::BAD_REQUEST,
AuthorizeFailed,
"Invalid token".to_string(),
),
};
let body = Json(json!({
"code": code,

View File

@ -1,5 +1,9 @@
use crate::utils::password::hash;
use crate::utils::{
jwt::{self, Claims},
password::hash,
};
use axum::{routing::post, Json, Router};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use validator::Validate;
@ -37,11 +41,20 @@ pub async fn registry(Json(user_param): Json<UserResigtry>) -> RouteResult<UserR
let hashed = hash(password).await?;
let iat = Utc::now().naive_utc();
let exp = (iat + chrono::naive::Days::new(7)).and_utc().timestamp() as usize;
let claims = Claims {
exp,
iat: iat.and_utc().timestamp() as usize,
sub: username.clone(),
};
let token = jwt::encode_jwt(&claims)?;
let data = UserResigtryRes {
username,
email,
password: hashed,
token: "abc".to_string(),
token,
};
let res = RouteResponse {
data,

View File

@ -1,8 +1,37 @@
use std::sync::LazyLock;
use axum::{async_trait, extract::FromRequestParts, http::request::Parts, RequestPartsExt};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use jsonwebtoken::{
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation,
};
use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};
use crate::error::AppError;
pub struct Keys {
pub encoding: EncodingKey,
pub decoding: DecodingKey,
}
impl Keys {
fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
}
pub static KEYS: LazyLock<Keys> = LazyLock::new(|| {
let secret = Alphanumeric.sample_string(&mut rand::thread_rng(), 32);
Keys::new(secret.as_bytes())
});
#[derive(Serialize, Deserialize, Debug)]
pub struct Claims {
// aud: String, // Optional. Audience
@ -13,23 +42,33 @@ pub struct Claims {
pub sub: String, // Optional. Subject (whom token refers to)
}
pub fn encode_jwt(claims: &Claims, key: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(key),
)
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Extract the token from the authorization header
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AppError::InvalidToken("Extract the token failed".into()))?;
// Decode the user data
let token_data = decode_jwt(bearer.token())
.map_err(|_| AppError::InvalidToken("Deocde the token failed".into()))?;
Ok(token_data.claims)
}
}
pub fn decode_jwt(
token: &str,
key: &[u8],
) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
decode::<Claims>(
token,
&DecodingKey::from_secret(key),
&Validation::default(),
)
pub fn encode_jwt(claims: &Claims) -> Result<String, jsonwebtoken::errors::Error> {
encode(&Header::new(Algorithm::HS256), &claims, &KEYS.encoding)
}
pub fn decode_jwt(token: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
decode::<Claims>(token, &KEYS.decoding, &Validation::default())
}
#[cfg(test)]
@ -40,7 +79,6 @@ mod tests {
#[test]
fn jwt_works() {
let key = "xfy";
let now = SystemTime::now();
let seconds_since_epoch: usize = now
.duration_since(UNIX_EPOCH)
@ -54,9 +92,9 @@ mod tests {
iat: seconds_since_epoch,
sub: sub.clone(),
};
let jwt = encode_jwt(&claims, key.as_ref()).unwrap();
let jwt = encode_jwt(&claims).unwrap();
let token_data = decode_jwt(&jwt, key.as_ref()).unwrap();
let token_data = decode_jwt(&jwt).unwrap();
println!("{token_data:?}");
assert_eq!(token_data.header.alg, Algorithm::HS256);