1use axum::extract::Request;
90use axum::http::StatusCode;
91use axum::middleware::Next;
92use axum::response::{IntoResponse, Response};
93use crate::orm::jwt::{JwtClaims, JwtEncoder};
94
95use crate::error::{BaseException, ErrorCode};
96
97pub const DEFAULT_ALLOW_ALL_ACTION: &[&str] = &["/passport/login", "/task/task/userClerk"];
106
107pub const DEFAULT_ISSUER: &str = "https://mall.ljclz.shop";
111
112#[cfg(test)]
125pub fn default_secret() -> String {
126 use rand::RngCore;
127 let mut bytes = [0u8; 32];
128 rand::rngs::OsRng.fill_bytes(&mut bytes);
129 bytes.iter().map(|b| format!("{:02x}", b)).collect()
131}
132
133pub const DEFAULT_SECRET: &str = "<must-set-SZ_JWT_SECRET-env>";
138
139pub const DEFAULT_EXPIRATION: u64 = 3600 * 24 * 30;
141
142#[derive(Clone)]
151pub struct AuthConfig {
152 pub secret: String,
154 pub issuer: String,
156 pub expiration: u64,
158 pub allow_all_action: Vec<String>,
162}
163
164impl std::fmt::Debug for AuthConfig {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("AuthConfig")
167 .field("secret", &"[REDACTED]")
168 .field("issuer", &self.issuer)
169 .field("expiration", &self.expiration)
170 .field("allow_all_action", &self.allow_all_action)
171 .finish()
172 }
173}
174
175impl Default for AuthConfig {
176 fn default() -> Self {
177 let secret = std::env::var("SZ_JWT_SECRET").unwrap_or_else(|_| {
180 #[cfg(test)]
181 {
182 default_secret()
183 }
184 #[cfg(not(test))]
185 {
186 panic!("SZ_JWT_SECRET 环境变量未设置 — 生产环境必须通过环境变量提供 JWT 密钥");
187 }
188 });
189 let issuer = std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string());
190 Self {
191 secret,
192 issuer,
193 expiration: DEFAULT_EXPIRATION,
194 allow_all_action: DEFAULT_ALLOW_ALL_ACTION
195 .iter()
196 .map(|s| s.to_string())
197 .collect(),
198 }
199 }
200}
201
202impl AuthConfig {
203 pub fn from_env() -> Result<Self, std::env::VarError> {
208 Ok(Self {
209 secret: std::env::var("SZ_JWT_SECRET")?,
210 issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string()),
211 expiration: DEFAULT_EXPIRATION,
212 allow_all_action: DEFAULT_ALLOW_ALL_ACTION
213 .iter()
214 .map(|s| s.to_string())
215 .collect(),
216 })
217 }
218
219 pub fn with_allow_all_action(mut self, allow: Vec<String>) -> Self {
221 self.allow_all_action = allow;
222 self
223 }
224
225 pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
227 self.secret = secret.into();
228 self
229 }
230
231 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
233 self.issuer = issuer.into();
234 self
235 }
236}
237
238#[tracing::instrument(skip_all)]
265pub async fn auth_middleware(
266 axum::extract::State(config): axum::extract::State<AuthConfig>,
267 req: Request,
268 next: Next,
269) -> Response {
270 let route_uri = extract_route_uri(&req);
272 if is_route_allowed(&route_uri, &config.allow_all_action) {
273 return next.run(req).await.into_response();
274 }
275
276 let auth_header = req.headers().get(axum::http::header::AUTHORIZATION);
278 let token = match auth_header {
279 Some(value) => {
280 let raw = value.to_str().unwrap_or("");
281 extract_token_from_header(raw)
283 }
284 None => None,
285 };
286
287 let token = match token {
288 Some(t) if !t.is_empty() => t,
289 _ => {
290 return base_exception_to_response(BaseException::not_login(
292 "缺少必要的参数,请重新登陆!",
293 ));
294 }
295 };
296
297 let encoder = JwtEncoder::new(&config.secret);
299 let claims = match encoder.decode(&token) {
300 Ok(c) => c,
301 Err(_) => {
302 return base_exception_to_response(BaseException::not_login(
305 "缺少必要的参数,请重新登陆!",
306 ));
307 }
308 };
309
310 if !verify_issuer(&claims, &config.issuer) {
312 return base_exception_to_response(BaseException::not_login("缺少必要的参数,请重新登陆!"));
313 }
314
315 let user_id = match claims.user_id {
317 Some(id) if id > 0 => id,
318 _ => {
319 return base_exception_to_response(BaseException::not_login("not_login"));
321 }
322 };
323
324 let mut req = req;
326 req.extensions_mut().insert(AuthenticatedUser { user_id });
327 next.run(req).await.into_response()
328}
329
330pub fn base_exception_to_response(exc: BaseException) -> Response {
335 let http_status = ErrorCode::from(exc.code).http_status();
336 let body = exc.to_json().to_string();
337 (
338 StatusCode::from_u16(http_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
339 [(
340 axum::http::header::CONTENT_TYPE,
341 "application/json; charset=utf-8",
342 )],
343 body,
344 )
345 .into_response()
346}
347
348#[derive(Debug, Clone, Copy)]
350pub struct AuthenticatedUser {
351 pub user_id: i64,
353}
354
355pub fn extract_token_from_header(header: &str) -> Option<String> {
370 let trimmed = header.trim();
371 if trimmed.is_empty() {
372 return None;
373 }
374 let lower = trimmed.to_lowercase();
377 if let Some(suffix_len) = lower.strip_prefix("bearer ").map(|s| s.len()) {
378 let rest = &trimmed[trimmed.len() - suffix_len..];
380 Some(rest.trim().to_string())
381 } else if let Some(suffix_len) = lower.strip_prefix("bearer").map(|s| s.len()) {
382 let rest = &trimmed[trimmed.len() - suffix_len..];
384 Some(rest.trim().to_string())
385 } else {
386 Some(trimmed.to_string())
388 }
389}
390
391pub fn extract_route_uri(req: &Request) -> String {
400 req.uri().path().to_string()
401}
402
403pub fn is_route_allowed(route_uri: &str, allow_list: &[String]) -> bool {
411 for pattern in allow_list {
412 if pattern == route_uri {
413 return true;
414 }
415 if pattern.contains('*') && wildcard_match(pattern, route_uri) {
416 return true;
417 }
418 }
419 false
420}
421
422pub fn wildcard_match(pattern: &str, text: &str) -> bool {
428 simple_wildcard_match(pattern, text)
429}
430
431fn simple_wildcard_match(pattern: &str, text: &str) -> bool {
435 let p: Vec<char> = pattern.chars().collect();
436 let t: Vec<char> = text.chars().collect();
437 let m = p.len();
438 let n = t.len();
439
440 let mut dp = vec![vec![false; n + 1]; m + 1];
442 dp[0][0] = true;
443
444 for i in 1..=m {
446 if p[i - 1] == '*' {
447 dp[i][0] = dp[i - 1][0];
448 }
449 }
450
451 for i in 1..=m {
452 for j in 1..=n {
453 if p[i - 1] == '*' {
454 dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
456 } else if p[i - 1] == t[j - 1] {
457 dp[i][j] = dp[i - 1][j - 1];
458 }
459 }
460 }
461
462 dp[m][n]
463}
464
465#[tracing::instrument(skip(claims))]
477pub fn verify_issuer(claims: &JwtClaims, expected_issuer: &str) -> bool {
478 match &claims.iss {
479 Some(iss) => iss == expected_issuer,
480 None => false,
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use axum::body::Body;
488 use axum::http::StatusCode;
489 use axum::Router;
490 use http_body_util::BodyExt;
491 use tower::ServiceExt;
492
493 async fn read_body(resp: Response) -> String {
498 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
499 String::from_utf8(bytes.to_vec()).unwrap()
500 }
501
502 fn make_request_with_uri(method: &str, uri: &str) -> Request {
503 Request::builder()
504 .method(method)
505 .uri(uri)
506 .body(Body::empty())
507 .unwrap()
508 }
509
510 fn make_request_with_auth(method: &str, uri: &str, auth: &str) -> Request {
511 Request::builder()
512 .method(method)
513 .uri(uri)
514 .header("Authorization", auth)
515 .body(Body::empty())
516 .unwrap()
517 }
518
519 fn make_test_token(secret: &str, issuer: &str, user_id: i64, exp_offset_secs: i64) -> String {
521 let encoder = JwtEncoder::new(secret);
522 let now = std::time::SystemTime::now()
523 .duration_since(std::time::UNIX_EPOCH)
524 .unwrap()
525 .as_secs() as i64;
526 let claims = JwtClaims::new("test_user", now + exp_offset_secs)
527 .with_issuer(issuer)
528 .with_user_id(user_id);
529 encoder.encode(&claims).expect("encode token")
530 }
531
532 #[test]
537 fn test_extract_token_from_header_with_bearer_prefix() {
538 let token = extract_token_from_header("Bearer abc123");
540 assert_eq!(token, Some("abc123".to_string()));
541 }
542
543 #[test]
544 fn test_extract_token_from_header_with_lowercase_bearer() {
545 let token = extract_token_from_header("bearer abc123");
547 assert_eq!(token, Some("abc123".to_string()));
548 }
549
550 #[test]
551 fn test_extract_token_from_header_with_uppercase_bearer() {
552 let token = extract_token_from_header("BEARER abc123");
554 assert_eq!(token, Some("abc123".to_string()));
555 }
556
557 #[test]
558 fn test_extract_token_from_header_without_bearer_prefix() {
559 let token = extract_token_from_header("abc123");
561 assert_eq!(token, Some("abc123".to_string()));
562 }
563
564 #[test]
565 fn test_extract_token_from_header_with_empty_string() {
566 let token = extract_token_from_header("");
567 assert_eq!(token, None);
568 }
569
570 #[test]
571 fn test_extract_token_from_header_with_only_whitespace() {
572 let token = extract_token_from_header(" ");
573 assert_eq!(token, None);
574 }
575
576 #[test]
577 fn test_extract_token_from_header_with_bearer_no_space() {
578 let token = extract_token_from_header("bearerabc");
580 assert_eq!(token, Some("abc".to_string()));
581 }
582
583 #[test]
584 fn test_extract_token_from_header_trims_whitespace() {
585 let token = extract_token_from_header(" Bearer abc123 ");
587 assert_eq!(token, Some("abc123".to_string()));
588 }
589
590 #[test]
595 fn test_is_route_allowed_exact_match() {
596 let allow = vec!["/passport/login".to_string()];
597 assert!(is_route_allowed("/passport/login", &allow));
598 assert!(!is_route_allowed("/passport/logout", &allow));
599 }
600
601 #[test]
602 fn test_is_route_allowed_multiple_entries() {
603 let allow = vec![
604 "/passport/login".to_string(),
605 "/task/task/userClerk".to_string(),
606 ];
607 assert!(is_route_allowed("/passport/login", &allow));
608 assert!(is_route_allowed("/task/task/userClerk", &allow));
609 assert!(!is_route_allowed("/passport/logout", &allow));
610 }
611
612 #[test]
613 fn test_is_route_allowed_wildcard_suffix() {
614 let allow = vec!["/upload.library/*".to_string()];
616 assert!(is_route_allowed("/upload.library/any", &allow));
617 assert!(is_route_allowed("/upload.library/sub/deep", &allow));
618 assert!(!is_route_allowed("/upload.library", &allow)); assert!(!is_route_allowed("/other/path", &allow));
620 }
621
622 #[test]
623 fn test_is_route_allowed_empty_list() {
624 let allow: Vec<String> = vec![];
625 assert!(!is_route_allowed("/any/path", &allow));
626 }
627
628 #[test]
629 fn test_wildcard_match_plain() {
630 assert!(wildcard_match("/upload/*", "/upload/any"));
631 assert!(wildcard_match("/upload/*", "/upload/sub/deep"));
632 assert!(!wildcard_match("/upload/*", "/other/any"));
633 }
634
635 #[test]
636 fn test_wildcard_match_exact_no_star() {
637 assert!(wildcard_match("/passport/login", "/passport/login"));
639 assert!(!wildcard_match("/passport/login", "/passport/logout"));
640 }
641
642 #[test]
643 fn test_wildcard_match_multiple_stars() {
644 assert!(wildcard_match("/*/*", "/a/b"));
645 assert!(wildcard_match("/*/*", "/abc/def"));
646 assert!(!wildcard_match("/*/*", "/a"));
647 }
648
649 #[test]
650 fn test_wildcard_match_star_at_end() {
651 assert!(wildcard_match("/api/*", "/api/v1/users"));
652 assert!(wildcard_match("/api/*", "/api/"));
653 assert!(!wildcard_match("/api/*", "/api"));
654 }
655
656 #[test]
657 fn test_wildcard_match_empty_pattern_and_text() {
658 assert!(wildcard_match("", ""));
659 assert!(!wildcard_match("", "abc"));
660 assert!(!wildcard_match("abc", ""));
661 }
662
663 #[test]
664 fn test_wildcard_match_star_only() {
665 assert!(wildcard_match("*", ""));
667 assert!(wildcard_match("*", "anything"));
668 assert!(wildcard_match("*", "/path/to/anything"));
669 }
670
671 #[test]
676 fn test_verify_issuer_matches() {
677 let claims = JwtClaims::new("user", 9999999999).with_issuer("https://mall.ljclz.shop");
678 assert!(verify_issuer(&claims, "https://mall.ljclz.shop"));
679 }
680
681 #[test]
682 fn test_verify_issuer_mismatch() {
683 let claims = JwtClaims::new("user", 9999999999).with_issuer("https://evil.com");
684 assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
685 }
686
687 #[test]
688 fn test_verify_issuer_missing() {
689 let claims = JwtClaims::new("user", 9999999999);
691 assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
692 }
693
694 #[test]
699 fn test_auth_config_default_matches_php() {
700 let config = AuthConfig::default();
702 assert_eq!(config.secret.len(), 64, "测试模式 secret 应为 64 字符随机密钥");
705 assert_eq!(config.issuer, "https://mall.ljclz.shop");
706 assert_eq!(config.expiration, 3600 * 24 * 30);
707 assert_eq!(
709 config.allow_all_action,
710 vec![
711 "/passport/login".to_string(),
712 "/task/task/userClerk".to_string(),
713 ]
714 );
715 }
716
717 #[test]
718 fn test_auth_config_default_allow_all_action_constant() {
719 assert_eq!(DEFAULT_ALLOW_ALL_ACTION.len(), 2);
721 assert_eq!(DEFAULT_ALLOW_ALL_ACTION[0], "/passport/login");
722 assert_eq!(DEFAULT_ALLOW_ALL_ACTION[1], "/task/task/userClerk");
723 }
724
725 #[test]
726 fn test_auth_config_builder_methods() {
727 let config = AuthConfig::default()
728 .with_secret("custom-secret")
729 .with_issuer("https://custom.com")
730 .with_allow_all_action(vec!["/custom/login".to_string()]);
731
732 assert_eq!(config.secret, "custom-secret");
733 assert_eq!(config.issuer, "https://custom.com");
734 assert_eq!(config.allow_all_action, vec!["/custom/login".to_string()]);
735 }
736
737 #[test]
738 fn test_auth_default_constants_match_php() {
739 assert_eq!(DEFAULT_ISSUER, "https://mall.ljclz.shop");
741 assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
743 assert_eq!(DEFAULT_EXPIRATION, 3600 * 24 * 30);
744 }
745
746 #[test]
751 fn test_extract_route_uri_strips_query_string() {
752 let req = Request::builder()
753 .uri("/passport/login?foo=bar&baz=qux")
754 .body(Body::empty())
755 .unwrap();
756 assert_eq!(extract_route_uri(&req), "/passport/login");
757 }
758
759 #[test]
760 fn test_extract_route_uri_no_query() {
761 let req = Request::builder()
762 .uri("/api/users")
763 .body(Body::empty())
764 .unwrap();
765 assert_eq!(extract_route_uri(&req), "/api/users");
766 }
767
768 #[test]
769 fn test_extract_route_uri_root() {
770 let req = Request::builder().uri("/").body(Body::empty()).unwrap();
771 assert_eq!(extract_route_uri(&req), "/");
772 }
773
774 fn build_app(config: AuthConfig) -> Router {
780 Router::new()
781 .route("/protected", axum::routing::get(|| async { "protected" }))
782 .route("/passport/login", axum::routing::get(|| async { "login" }))
783 .route(
784 "/upload.library/test",
785 axum::routing::get(|| async { "upload" }),
786 )
787 .layer(axum::middleware::from_fn_with_state(
788 config,
789 auth_middleware,
790 ))
791 }
792
793 #[tokio::test]
794 async fn test_auth_middleware_allows_whitelisted_route() {
795 let app = build_app(AuthConfig::default());
797 let resp = app
798 .oneshot(make_request_with_uri("GET", "/passport/login"))
799 .await
800 .unwrap();
801 assert_eq!(resp.status(), StatusCode::OK);
802 let body = read_body(resp).await;
803 assert_eq!(body, "login");
804 }
805
806 #[tokio::test]
807 async fn test_auth_middleware_rejects_missing_authorization_header() {
808 let app = build_app(AuthConfig::default());
810 let resp = app
811 .oneshot(make_request_with_uri("GET", "/protected"))
812 .await
813 .unwrap();
814 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
816 assert!(body.contains("\"code\":-1"));
817 assert!(body.contains("缺少必要的参数,请重新登陆!"));
818 }
819
820 #[tokio::test]
821 async fn test_auth_middleware_rejects_empty_authorization_header() {
822 let app = build_app(AuthConfig::default());
824 let resp = app
825 .oneshot(make_request_with_auth("GET", "/protected", ""))
826 .await
827 .unwrap();
828 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
829 let body = read_body(resp).await;
830 assert!(body.contains("\"code\":-1"));
831 }
832
833 #[tokio::test]
834 async fn test_auth_middleware_rejects_invalid_token() {
835 let app = build_app(AuthConfig::default());
837 let resp = app
838 .oneshot(make_request_with_auth(
839 "GET",
840 "/protected",
841 "invalid.token.here",
842 ))
843 .await
844 .unwrap();
845 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
846 let body = read_body(resp).await;
847 assert!(body.contains("\"code\":-1"));
848 assert!(body.contains("缺少必要的参数,请重新登陆!"));
849 }
850
851 #[tokio::test]
852 async fn test_auth_middleware_rejects_expired_token() {
853 let config = AuthConfig::default();
855 let token = make_test_token(&config.secret, &config.issuer, 1, -3600);
857 let app = build_app(config.clone());
858 let resp = app
859 .oneshot(make_request_with_auth("GET", "/protected", &token))
860 .await
861 .unwrap();
862 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
863 let body = read_body(resp).await;
864 assert!(body.contains("\"code\":-1"));
865 }
866
867 #[tokio::test]
868 async fn test_auth_middleware_rejects_wrong_secret_token() {
869 let config = AuthConfig::default();
871 let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
872 let app = build_app(config.clone());
873 let resp = app
874 .oneshot(make_request_with_auth("GET", "/protected", &token))
875 .await
876 .unwrap();
877 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
878 }
879
880 #[tokio::test]
881 async fn test_auth_middleware_rejects_wrong_issuer_token() {
882 let config = AuthConfig::default();
884 let token = make_test_token(&config.secret, "https://evil.com", 1, 3600);
885 let app = build_app(config.clone());
886 let resp = app
887 .oneshot(make_request_with_auth("GET", "/protected", &token))
888 .await
889 .unwrap();
890 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
891 let body = read_body(resp).await;
892 assert!(body.contains("缺少必要的参数,请重新登陆!"));
893 }
894
895 #[tokio::test]
896 async fn test_auth_middleware_rejects_token_without_user_id() {
897 let config = AuthConfig::default();
899 let encoder = JwtEncoder::new(&config.secret);
900 let now = std::time::SystemTime::now()
901 .duration_since(std::time::UNIX_EPOCH)
902 .unwrap()
903 .as_secs() as i64;
904 let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
906 let token = encoder.encode(&claims).unwrap();
907 let app = build_app(config.clone());
908 let resp = app
909 .oneshot(make_request_with_auth("GET", "/protected", &token))
910 .await
911 .unwrap();
912 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
913 let body = read_body(resp).await;
914 assert!(body.contains("\"code\":-1"));
915 assert!(body.contains("not_login"));
916 }
917
918 #[tokio::test]
919 async fn test_auth_middleware_rejects_token_with_zero_user_id() {
920 let config = AuthConfig::default();
922 let token = make_test_token(&config.secret, &config.issuer, 0, 3600);
923 let app = build_app(config.clone());
924 let resp = app
925 .oneshot(make_request_with_auth("GET", "/protected", &token))
926 .await
927 .unwrap();
928 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
929 let body = read_body(resp).await;
930 assert!(body.contains("not_login"));
931 }
932
933 #[tokio::test]
934 async fn test_auth_middleware_accepts_valid_token_with_bearer_prefix() {
935 let config = AuthConfig::default();
937 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
938 let app = build_app(config.clone());
939 let resp = app
940 .oneshot(make_request_with_auth(
941 "GET",
942 "/protected",
943 &format!("Bearer {}", token),
944 ))
945 .await
946 .unwrap();
947 assert_eq!(resp.status(), StatusCode::OK);
948 let body = read_body(resp).await;
949 assert_eq!(body, "protected");
950 }
951
952 #[tokio::test]
953 async fn test_auth_middleware_accepts_valid_token_without_bearer_prefix() {
954 let config = AuthConfig::default();
956 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
957 let app = build_app(config.clone());
958 let resp = app
959 .oneshot(make_request_with_auth("GET", "/protected", &token))
960 .await
961 .unwrap();
962 assert_eq!(resp.status(), StatusCode::OK);
963 }
964
965 #[tokio::test]
966 async fn test_auth_middleware_accepts_lowercase_bearer_prefix() {
967 let config = AuthConfig::default();
969 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
970 let app = build_app(config.clone());
971 let resp = app
972 .oneshot(make_request_with_auth(
973 "GET",
974 "/protected",
975 &format!("bearer {}", token),
976 ))
977 .await
978 .unwrap();
979 assert_eq!(resp.status(), StatusCode::OK);
980 }
981
982 #[tokio::test]
983 async fn test_auth_middleware_supports_wildcard_whitelist() {
984 let config =
986 AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
987 let app = build_app(config);
988 let resp = app
989 .oneshot(make_request_with_uri("GET", "/upload.library/test"))
990 .await
991 .unwrap();
992 assert_eq!(resp.status(), StatusCode::OK);
993 let body = read_body(resp).await;
994 assert_eq!(body, "upload");
995 }
996
997 #[tokio::test]
998 async fn test_auth_middleware_wildcard_does_not_overmatch() {
999 let config =
1001 AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
1002 let app = build_app(config);
1003 let resp = app
1005 .oneshot(make_request_with_uri("GET", "/protected"))
1006 .await
1007 .unwrap();
1008 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1009 }
1010
1011 #[tokio::test]
1012 async fn test_auth_middleware_injects_user_id_into_extensions() {
1013 let config = AuthConfig::default();
1015 let token = make_test_token(&config.secret, &config.issuer, 99, 3600);
1016 let app = Router::new()
1017 .route(
1018 "/protected",
1019 axum::routing::get(|req: Request| async move {
1020 let user = req.extensions().get::<AuthenticatedUser>().unwrap();
1021 format!("user_id:{}", user.user_id)
1022 }),
1023 )
1024 .layer(axum::middleware::from_fn_with_state(
1025 config.clone(),
1026 auth_middleware,
1027 ));
1028 let resp = app
1029 .oneshot(make_request_with_auth("GET", "/protected", &token))
1030 .await
1031 .unwrap();
1032 assert_eq!(resp.status(), StatusCode::OK);
1033 let body = read_body(resp).await;
1034 assert_eq!(body, "user_id:99");
1035 }
1036
1037 #[tokio::test]
1038 async fn test_auth_middleware_returns_correct_error_code_for_missing_token() {
1039 let app = build_app(AuthConfig::default());
1041 let resp = app
1042 .oneshot(make_request_with_uri("GET", "/protected"))
1043 .await
1044 .unwrap();
1045 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
1047 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1049 assert_eq!(json["code"], -1);
1050 assert_eq!(json["msg"], "缺少必要的参数,请重新登陆!");
1051 assert_eq!(json["data"], serde_json::json!({}));
1052 }
1053
1054 #[tokio::test]
1055 async fn test_auth_middleware_returns_correct_error_code_for_not_login() {
1056 let config = AuthConfig::default();
1058 let encoder = JwtEncoder::new(&config.secret);
1059 let now = std::time::SystemTime::now()
1060 .duration_since(std::time::UNIX_EPOCH)
1061 .unwrap()
1062 .as_secs() as i64;
1063 let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
1064 let token = encoder.encode(&claims).unwrap();
1065 let app = build_app(config.clone());
1066 let resp = app
1067 .oneshot(make_request_with_auth("GET", "/protected", &token))
1068 .await
1069 .unwrap();
1070 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1071 let body = read_body(resp).await;
1072 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1073 assert_eq!(json["code"], -1);
1074 assert_eq!(json["msg"], "not_login");
1075 }
1076
1077 #[tokio::test]
1078 async fn test_auth_middleware_custom_secret_and_issuer() {
1079 let config = AuthConfig::default()
1081 .with_secret("custom-secret")
1082 .with_issuer("https://custom.com");
1083 let token = make_test_token("custom-secret", "https://custom.com", 1, 3600);
1084 let app = build_app(config);
1085 let resp = app
1086 .oneshot(make_request_with_auth("GET", "/protected", &token))
1087 .await
1088 .unwrap();
1089 assert_eq!(resp.status(), StatusCode::OK);
1090 }
1091
1092 #[tokio::test]
1093 async fn test_auth_middleware_rejects_token_signed_with_default_secret_when_custom_configured()
1094 {
1095 let config = AuthConfig::default().with_secret("custom-secret");
1097 let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
1098 let app = build_app(config);
1099 let resp = app
1100 .oneshot(make_request_with_auth("GET", "/protected", &token))
1101 .await
1102 .unwrap();
1103 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1104 }
1105
1106 #[tokio::test]
1107 async fn test_auth_middleware_preserves_query_string_in_route_match() {
1108 let app = build_app(AuthConfig::default());
1110 let resp = app
1111 .oneshot(make_request_with_uri(
1112 "GET",
1113 "/passport/login?redirect=/home",
1114 ))
1115 .await
1116 .unwrap();
1117 assert_eq!(resp.status(), StatusCode::OK);
1118 }
1119
1120 #[tokio::test]
1121 async fn test_auth_middleware_handles_token_with_only_bearer_prefix() {
1122 let app = build_app(AuthConfig::default());
1124 let resp = app
1125 .oneshot(make_request_with_auth("GET", "/protected", "Bearer "))
1126 .await
1127 .unwrap();
1128 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1129 }
1130
1131 #[test]
1136 fn test_authorization_header_name_aligns_with_php() {
1137 let header_name = axum::http::header::AUTHORIZATION;
1141 assert_eq!(header_name.as_str(), "authorization");
1142 }
1144
1145 #[test]
1150 fn test_php_default_allow_all_action_matches_rust() {
1151 let php_allow = vec!["/passport/login", "/task/task/userClerk"];
1153 assert_eq!(php_allow, DEFAULT_ALLOW_ALL_ACTION);
1155 }
1156
1157 #[test]
1158 fn test_php_jwt_config_matches_rust() {
1159 let php_issuer = "https://mall.ljclz.shop";
1161 let php_expire = 3600 * 24 * 30;
1162
1163 assert_eq!(php_issuer, DEFAULT_ISSUER);
1165 assert_eq!(php_expire, DEFAULT_EXPIRATION);
1166 assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
1168 }
1169}