use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use sz_rust_orm_facade::jwt::{JwtClaims, JwtEncoder};
use sz_rust_http_facade::{BaseException, ErrorCode};
pub const DEFAULT_ALLOW_ALL_ACTION: &[&str] = &["/passport/login", "/task/task/userClerk"];
pub const DEFAULT_ISSUER: &str = "https://mall.ljclz.shop";
#[cfg(test)]
pub fn default_secret() -> String {
use rand::RngCore;
let mut bytes = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut bytes);
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub const DEFAULT_SECRET: &str = "<must-set-SZ_JWT_SECRET-env>";
pub const DEFAULT_EXPIRATION: u64 = 3600 * 24 * 30;
pub const MIN_SECRET_LEN: usize = 32;
#[derive(Clone)]
pub struct AuthConfig {
pub secret: String,
pub issuer: String,
pub expiration: u64,
pub allow_all_action: Vec<String>,
}
impl std::fmt::Debug for AuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthConfig")
.field("secret", &"[REDACTED]")
.field("issuer", &self.issuer)
.field("expiration", &self.expiration)
.field("allow_all_action", &self.allow_all_action)
.finish()
}
}
impl Default for AuthConfig {
fn default() -> Self {
let secret = std::env::var("SZ_JWT_SECRET").unwrap_or_else(|_| {
#[cfg(test)]
{
default_secret()
}
#[cfg(not(test))]
{
panic!("SZ_JWT_SECRET 环境变量未设置 — 生产环境必须通过环境变量提供 JWT 密钥");
}
});
validate_secret(&secret);
let issuer = std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string());
Self {
secret,
issuer,
expiration: DEFAULT_EXPIRATION,
allow_all_action: DEFAULT_ALLOW_ALL_ACTION
.iter()
.map(|s| s.to_string())
.collect(),
}
}
}
fn validate_secret(secret: &str) {
if secret.len() < MIN_SECRET_LEN {
panic!(
"SZ_JWT_SECRET 长度不足(当前 {} 字节,要求 ≥ {} 字节)— 请使用强随机密钥",
secret.len(),
MIN_SECRET_LEN
);
}
}
impl AuthConfig {
pub fn from_env() -> Result<Self, std::env::VarError> {
let secret = std::env::var("SZ_JWT_SECRET")?;
validate_secret(&secret);
Ok(Self {
secret,
issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string()),
expiration: DEFAULT_EXPIRATION,
allow_all_action: DEFAULT_ALLOW_ALL_ACTION
.iter()
.map(|s| s.to_string())
.collect(),
})
}
pub fn with_allow_all_action(mut self, allow: Vec<String>) -> Self {
self.allow_all_action = allow;
self
}
pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
let secret = secret.into();
validate_secret(&secret);
self.secret = secret;
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = issuer.into();
self
}
}
#[tracing::instrument(skip_all)]
pub async fn auth_middleware(
axum::extract::State(config): axum::extract::State<AuthConfig>,
req: Request,
next: Next,
) -> Response {
let route_uri = extract_route_uri(&req);
if is_route_allowed(&route_uri, &config.allow_all_action) {
return next.run(req).await.into_response();
}
let auth_header = req.headers().get(axum::http::header::AUTHORIZATION);
let token = match auth_header {
Some(value) => {
let raw = value.to_str().unwrap_or("");
extract_token_from_header(raw)
}
None => None,
};
let token = match token {
Some(t) if !t.is_empty() => t,
_ => {
return base_exception_to_response(BaseException::not_login(
"缺少必要的参数,请重新登陆!",
));
}
};
let encoder = JwtEncoder::new(&config.secret);
let claims = match encoder.decode(&token) {
Ok(c) => c,
Err(_) => {
return base_exception_to_response(BaseException::not_login(
"缺少必要的参数,请重新登陆!",
));
}
};
if !verify_issuer(&claims, &config.issuer) {
return base_exception_to_response(BaseException::not_login("缺少必要的参数,请重新登陆!"));
}
let user_id = match claims.user_id {
Some(id) if id > 0 => id,
_ => {
return base_exception_to_response(BaseException::not_login("not_login"));
}
};
let mut req = req;
req.extensions_mut().insert(AuthenticatedUser { user_id });
next.run(req).await.into_response()
}
pub fn base_exception_to_response(exc: BaseException) -> Response {
let http_status = ErrorCode::from(exc.code).http_status();
let body = exc.to_json().to_string();
(
StatusCode::from_u16(http_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
[(
axum::http::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)],
body,
)
.into_response()
}
#[derive(Debug, Clone, Copy)]
pub struct AuthenticatedUser {
pub user_id: i64,
}
pub fn extract_token_from_header(header: &str) -> Option<String> {
let trimmed = header.trim();
if trimmed.is_empty() {
return None;
}
let lower = trimmed.to_lowercase();
if let Some(suffix_len) = lower.strip_prefix("bearer ").map(|s| s.len()) {
let rest = &trimmed[trimmed.len() - suffix_len..];
Some(rest.trim().to_string())
} else if let Some(suffix_len) = lower.strip_prefix("bearer").map(|s| s.len()) {
let rest = &trimmed[trimmed.len() - suffix_len..];
Some(rest.trim().to_string())
} else {
Some(trimmed.to_string())
}
}
pub fn extract_route_uri(req: &Request) -> String {
req.uri().path().to_string()
}
pub fn is_route_allowed(route_uri: &str, allow_list: &[String]) -> bool {
for pattern in allow_list {
if pattern == route_uri {
return true;
}
if pattern.contains('*') && wildcard_match(pattern, route_uri) {
return true;
}
}
false
}
pub fn wildcard_match(pattern: &str, text: &str) -> bool {
simple_wildcard_match(pattern, text)
}
fn simple_wildcard_match(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
let m = p.len();
let n = t.len();
let mut dp = vec![vec![false; n + 1]; m + 1];
dp[0][0] = true;
for i in 1..=m {
if p[i - 1] == '*' {
dp[i][0] = dp[i - 1][0];
}
}
for i in 1..=m {
for j in 1..=n {
if p[i - 1] == '*' {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else if p[i - 1] == t[j - 1] {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
dp[m][n]
}
#[tracing::instrument(skip(claims))]
pub fn verify_issuer(claims: &JwtClaims, expected_issuer: &str) -> bool {
match &claims.iss {
Some(iss) => iss == expected_issuer,
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::StatusCode;
use axum::Router;
use http_body_util::BodyExt;
use tower::ServiceExt;
async fn read_body(resp: Response) -> String {
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
fn make_request_with_uri(method: &str, uri: &str) -> Request {
Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.unwrap()
}
fn make_request_with_auth(method: &str, uri: &str, auth: &str) -> Request {
Request::builder()
.method(method)
.uri(uri)
.header("Authorization", auth)
.body(Body::empty())
.unwrap()
}
fn make_test_token(secret: &str, issuer: &str, user_id: i64, exp_offset_secs: i64) -> String {
let encoder = JwtEncoder::new(secret);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let claims = JwtClaims::new("test_user", now + exp_offset_secs)
.with_issuer(issuer)
.with_user_id(user_id);
encoder.encode(&claims).expect("encode token")
}
#[test]
fn test_extract_token_from_header_with_bearer_prefix() {
let token = extract_token_from_header("Bearer abc123");
assert_eq!(token, Some("abc123".to_string()));
}
#[test]
fn test_extract_token_from_header_with_lowercase_bearer() {
let token = extract_token_from_header("bearer abc123");
assert_eq!(token, Some("abc123".to_string()));
}
#[test]
fn test_extract_token_from_header_with_uppercase_bearer() {
let token = extract_token_from_header("BEARER abc123");
assert_eq!(token, Some("abc123".to_string()));
}
#[test]
fn test_extract_token_from_header_without_bearer_prefix() {
let token = extract_token_from_header("abc123");
assert_eq!(token, Some("abc123".to_string()));
}
#[test]
fn test_extract_token_from_header_with_empty_string() {
let token = extract_token_from_header("");
assert_eq!(token, None);
}
#[test]
fn test_extract_token_from_header_with_only_whitespace() {
let token = extract_token_from_header(" ");
assert_eq!(token, None);
}
#[test]
fn test_extract_token_from_header_with_bearer_no_space() {
let token = extract_token_from_header("bearerabc");
assert_eq!(token, Some("abc".to_string()));
}
#[test]
fn test_extract_token_from_header_trims_whitespace() {
let token = extract_token_from_header(" Bearer abc123 ");
assert_eq!(token, Some("abc123".to_string()));
}
#[test]
fn test_is_route_allowed_exact_match() {
let allow = vec!["/passport/login".to_string()];
assert!(is_route_allowed("/passport/login", &allow));
assert!(!is_route_allowed("/passport/logout", &allow));
}
#[test]
fn test_is_route_allowed_multiple_entries() {
let allow = vec![
"/passport/login".to_string(),
"/task/task/userClerk".to_string(),
];
assert!(is_route_allowed("/passport/login", &allow));
assert!(is_route_allowed("/task/task/userClerk", &allow));
assert!(!is_route_allowed("/passport/logout", &allow));
}
#[test]
fn test_is_route_allowed_wildcard_suffix() {
let allow = vec!["/upload.library/*".to_string()];
assert!(is_route_allowed("/upload.library/any", &allow));
assert!(is_route_allowed("/upload.library/sub/deep", &allow));
assert!(!is_route_allowed("/upload.library", &allow)); assert!(!is_route_allowed("/other/path", &allow));
}
#[test]
fn test_is_route_allowed_empty_list() {
let allow: Vec<String> = vec![];
assert!(!is_route_allowed("/any/path", &allow));
}
#[test]
fn test_wildcard_match_plain() {
assert!(wildcard_match("/upload/*", "/upload/any"));
assert!(wildcard_match("/upload/*", "/upload/sub/deep"));
assert!(!wildcard_match("/upload/*", "/other/any"));
}
#[test]
fn test_wildcard_match_exact_no_star() {
assert!(wildcard_match("/passport/login", "/passport/login"));
assert!(!wildcard_match("/passport/login", "/passport/logout"));
}
#[test]
fn test_wildcard_match_multiple_stars() {
assert!(wildcard_match("/*/*", "/a/b"));
assert!(wildcard_match("/*/*", "/abc/def"));
assert!(!wildcard_match("/*/*", "/a"));
}
#[test]
fn test_wildcard_match_star_at_end() {
assert!(wildcard_match("/api/*", "/api/v1/users"));
assert!(wildcard_match("/api/*", "/api/"));
assert!(!wildcard_match("/api/*", "/api"));
}
#[test]
fn test_wildcard_match_empty_pattern_and_text() {
assert!(wildcard_match("", ""));
assert!(!wildcard_match("", "abc"));
assert!(!wildcard_match("abc", ""));
}
#[test]
fn test_wildcard_match_star_only() {
assert!(wildcard_match("*", ""));
assert!(wildcard_match("*", "anything"));
assert!(wildcard_match("*", "/path/to/anything"));
}
#[test]
fn test_verify_issuer_matches() {
let claims = JwtClaims::new("user", 9999999999).with_issuer("https://mall.ljclz.shop");
assert!(verify_issuer(&claims, "https://mall.ljclz.shop"));
}
#[test]
fn test_verify_issuer_mismatch() {
let claims = JwtClaims::new("user", 9999999999).with_issuer("https://evil.com");
assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
}
#[test]
fn test_verify_issuer_missing() {
let claims = JwtClaims::new("user", 9999999999);
assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
}
#[test]
fn test_auth_config_default_matches_php() {
let config = AuthConfig::default();
assert_eq!(
config.secret.len(),
64,
"测试模式 secret 应为 64 字符随机密钥"
);
assert_eq!(config.issuer, "https://mall.ljclz.shop");
assert_eq!(config.expiration, 3600 * 24 * 30);
assert_eq!(
config.allow_all_action,
vec![
"/passport/login".to_string(),
"/task/task/userClerk".to_string(),
]
);
}
#[test]
fn test_auth_config_default_allow_all_action_constant() {
assert_eq!(DEFAULT_ALLOW_ALL_ACTION.len(), 2);
assert_eq!(DEFAULT_ALLOW_ALL_ACTION[0], "/passport/login");
assert_eq!(DEFAULT_ALLOW_ALL_ACTION[1], "/task/task/userClerk");
}
#[test]
fn test_auth_config_builder_methods() {
let config = AuthConfig::default()
.with_secret("0123456789abcdef0123456789abcdef")
.with_issuer("https://custom.com")
.with_allow_all_action(vec!["/custom/login".to_string()]);
assert_eq!(config.secret, "0123456789abcdef0123456789abcdef");
assert_eq!(config.issuer, "https://custom.com");
assert_eq!(config.allow_all_action, vec!["/custom/login".to_string()]);
}
#[test]
#[should_panic(expected = "SZ_JWT_SECRET 长度不足")]
fn test_secret_too_short_panics() {
let _ = AuthConfig::default().with_secret("short");
}
#[test]
fn test_secret_exactly_min_length_ok() {
let config = AuthConfig::default().with_secret("0123456789abcdef0123456789abcdef");
assert_eq!(config.secret.len(), 32);
}
#[test]
fn test_auth_default_constants_match_php() {
assert_eq!(DEFAULT_ISSUER, "https://mall.ljclz.shop");
assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
assert_eq!(DEFAULT_EXPIRATION, 3600 * 24 * 30);
}
#[test]
fn test_extract_route_uri_strips_query_string() {
let req = Request::builder()
.uri("/passport/login?foo=bar&baz=qux")
.body(Body::empty())
.unwrap();
assert_eq!(extract_route_uri(&req), "/passport/login");
}
#[test]
fn test_extract_route_uri_no_query() {
let req = Request::builder()
.uri("/api/users")
.body(Body::empty())
.unwrap();
assert_eq!(extract_route_uri(&req), "/api/users");
}
#[test]
fn test_extract_route_uri_root() {
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
assert_eq!(extract_route_uri(&req), "/");
}
fn build_app(config: AuthConfig) -> Router {
Router::new()
.route("/protected", axum::routing::get(|| async { "protected" }))
.route("/passport/login", axum::routing::get(|| async { "login" }))
.route(
"/upload.library/test",
axum::routing::get(|| async { "upload" }),
)
.layer(axum::middleware::from_fn_with_state(
config,
auth_middleware,
))
}
#[tokio::test]
async fn test_auth_middleware_allows_whitelisted_route() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_uri("GET", "/passport/login"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, "login");
}
#[tokio::test]
async fn test_auth_middleware_rejects_missing_authorization_header() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_uri("GET", "/protected"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
assert!(body.contains("\"code\":-1"));
assert!(body.contains("缺少必要的参数,请重新登陆!"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_empty_authorization_header() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", ""))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("\"code\":-1"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_invalid_token() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_auth(
"GET",
"/protected",
"invalid.token.here",
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("\"code\":-1"));
assert!(body.contains("缺少必要的参数,请重新登陆!"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_expired_token() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 1, -3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("\"code\":-1"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_wrong_secret_token() {
let config = AuthConfig::default();
let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_auth_middleware_rejects_wrong_issuer_token() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, "https://evil.com", 1, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("缺少必要的参数,请重新登陆!"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_token_without_user_id() {
let config = AuthConfig::default();
let encoder = JwtEncoder::new(&config.secret);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
let token = encoder.encode(&claims).unwrap();
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("\"code\":-1"));
assert!(body.contains("not_login"));
}
#[tokio::test]
async fn test_auth_middleware_rejects_token_with_zero_user_id() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 0, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
assert!(body.contains("not_login"));
}
#[tokio::test]
async fn test_auth_middleware_accepts_valid_token_with_bearer_prefix() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth(
"GET",
"/protected",
&format!("Bearer {}", token),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, "protected");
}
#[tokio::test]
async fn test_auth_middleware_accepts_valid_token_without_bearer_prefix() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_accepts_lowercase_bearer_prefix() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth(
"GET",
"/protected",
&format!("bearer {}", token),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_supports_wildcard_whitelist() {
let config =
AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
let app = build_app(config);
let resp = app
.oneshot(make_request_with_uri("GET", "/upload.library/test"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, "upload");
}
#[tokio::test]
async fn test_auth_middleware_wildcard_does_not_overmatch() {
let config =
AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
let app = build_app(config);
let resp = app
.oneshot(make_request_with_uri("GET", "/protected"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_auth_middleware_injects_user_id_into_extensions() {
let config = AuthConfig::default();
let token = make_test_token(&config.secret, &config.issuer, 99, 3600);
let app = Router::new()
.route(
"/protected",
axum::routing::get(|req: Request| async move {
let user = req.extensions().get::<AuthenticatedUser>().unwrap();
format!("user_id:{}", user.user_id)
}),
)
.layer(axum::middleware::from_fn_with_state(
config.clone(),
auth_middleware,
));
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, "user_id:99");
}
#[tokio::test]
async fn test_auth_middleware_returns_correct_error_code_for_missing_token() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_uri("GET", "/protected"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
let json: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(json["code"], -1);
assert_eq!(json["msg"], "缺少必要的参数,请重新登陆!");
assert_eq!(json["data"], serde_json::json!({}));
}
#[tokio::test]
async fn test_auth_middleware_returns_correct_error_code_for_not_login() {
let config = AuthConfig::default();
let encoder = JwtEncoder::new(&config.secret);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
let token = encoder.encode(&claims).unwrap();
let app = build_app(config.clone());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = read_body(resp).await;
let json: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(json["code"], -1);
assert_eq!(json["msg"], "not_login");
}
#[tokio::test]
async fn test_auth_middleware_custom_secret_and_issuer() {
let config = AuthConfig::default()
.with_secret("0123456789abcdef0123456789abcdef")
.with_issuer("https://custom.com");
let token = make_test_token(
"0123456789abcdef0123456789abcdef",
"https://custom.com",
1,
3600,
);
let app = build_app(config);
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_rejects_token_signed_with_default_secret_when_custom_configured()
{
let config = AuthConfig::default().with_secret("0123456789abcdef0123456789abcdef");
let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
let app = build_app(config);
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_auth_middleware_preserves_query_string_in_route_match() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_uri(
"GET",
"/passport/login?redirect=/home",
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_handles_token_with_only_bearer_prefix() {
let app = build_app(AuthConfig::default());
let resp = app
.oneshot(make_request_with_auth("GET", "/protected", "Bearer "))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_authorization_header_name_aligns_with_php() {
let header_name = axum::http::header::AUTHORIZATION;
assert_eq!(header_name.as_str(), "authorization");
}
#[test]
fn test_php_default_allow_all_action_matches_rust() {
let php_allow = vec!["/passport/login", "/task/task/userClerk"];
assert_eq!(php_allow, DEFAULT_ALLOW_ALL_ACTION);
}
#[test]
fn test_php_jwt_config_matches_rust() {
let php_issuer = "https://mall.ljclz.shop";
let php_expire = 3600 * 24 * 30;
assert_eq!(php_issuer, DEFAULT_ISSUER);
assert_eq!(php_expire, DEFAULT_EXPIRATION);
assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
}
}