1use axum::extract::Request;
90use axum::http::StatusCode;
91use axum::middleware::Next;
92use axum::response::{IntoResponse, Response};
93use sz_orm_auth::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)]
119pub const DEFAULT_SECRET: &str = "shengzhuang";
120
121#[cfg(not(test))]
123pub const DEFAULT_SECRET: &str = "<must-set-SZ_JWT_SECRET-env>";
124
125pub const DEFAULT_EXPIRATION: u64 = 3600 * 24 * 30;
127
128#[derive(Debug, Clone)]
132pub struct AuthConfig {
133 pub secret: String,
135 pub issuer: String,
137 pub expiration: u64,
139 pub allow_all_action: Vec<String>,
143}
144
145impl Default for AuthConfig {
146 fn default() -> Self {
147 let secret = std::env::var("SZ_JWT_SECRET").unwrap_or_else(|_| {
150 #[cfg(test)]
151 {
152 DEFAULT_SECRET.to_string()
153 }
154 #[cfg(not(test))]
155 {
156 panic!("SZ_JWT_SECRET 环境变量未设置 — 生产环境必须通过环境变量提供 JWT 密钥");
157 }
158 });
159 let issuer = std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string());
160 Self {
161 secret,
162 issuer,
163 expiration: DEFAULT_EXPIRATION,
164 allow_all_action: DEFAULT_ALLOW_ALL_ACTION
165 .iter()
166 .map(|s| s.to_string())
167 .collect(),
168 }
169 }
170}
171
172impl AuthConfig {
173 pub fn from_env() -> Result<Self, std::env::VarError> {
178 Ok(Self {
179 secret: std::env::var("SZ_JWT_SECRET")?,
180 issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string()),
181 expiration: DEFAULT_EXPIRATION,
182 allow_all_action: DEFAULT_ALLOW_ALL_ACTION
183 .iter()
184 .map(|s| s.to_string())
185 .collect(),
186 })
187 }
188
189 pub fn with_allow_all_action(mut self, allow: Vec<String>) -> Self {
191 self.allow_all_action = allow;
192 self
193 }
194
195 pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
197 self.secret = secret.into();
198 self
199 }
200
201 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
203 self.issuer = issuer.into();
204 self
205 }
206}
207
208#[tracing::instrument(skip_all)]
235pub async fn auth_middleware(
236 axum::extract::State(config): axum::extract::State<AuthConfig>,
237 req: Request,
238 next: Next,
239) -> Response {
240 let route_uri = extract_route_uri(&req);
242 if is_route_allowed(&route_uri, &config.allow_all_action) {
243 return next.run(req).await.into_response();
244 }
245
246 let auth_header = req.headers().get(axum::http::header::AUTHORIZATION);
248 let token = match auth_header {
249 Some(value) => {
250 let raw = value.to_str().unwrap_or("");
251 extract_token_from_header(raw)
253 }
254 None => None,
255 };
256
257 let token = match token {
258 Some(t) if !t.is_empty() => t,
259 _ => {
260 return base_exception_to_response(BaseException::not_login(
262 "缺少必要的参数,请重新登陆!",
263 ));
264 }
265 };
266
267 let encoder = JwtEncoder::new(&config.secret);
269 let claims = match encoder.decode(&token) {
270 Ok(c) => c,
271 Err(_) => {
272 return base_exception_to_response(BaseException::not_login(
275 "缺少必要的参数,请重新登陆!",
276 ));
277 }
278 };
279
280 if !verify_issuer(&claims, &config.issuer) {
282 return base_exception_to_response(BaseException::not_login("缺少必要的参数,请重新登陆!"));
283 }
284
285 let user_id = match claims.user_id {
287 Some(id) if id > 0 => id,
288 _ => {
289 return base_exception_to_response(BaseException::not_login("not_login"));
291 }
292 };
293
294 let mut req = req;
296 req.extensions_mut().insert(AuthenticatedUser { user_id });
297 next.run(req).await.into_response()
298}
299
300pub fn base_exception_to_response(exc: BaseException) -> Response {
305 let http_status = ErrorCode::from(exc.code).http_status();
306 let body = exc.to_json().to_string();
307 (
308 StatusCode::from_u16(http_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
309 [(
310 axum::http::header::CONTENT_TYPE,
311 "application/json; charset=utf-8",
312 )],
313 body,
314 )
315 .into_response()
316}
317
318#[derive(Debug, Clone, Copy)]
320pub struct AuthenticatedUser {
321 pub user_id: i64,
323}
324
325pub fn extract_token_from_header(header: &str) -> Option<String> {
340 let trimmed = header.trim();
341 if trimmed.is_empty() {
342 return None;
343 }
344 let lower = trimmed.to_lowercase();
347 if let Some(suffix_len) = lower.strip_prefix("bearer ").map(|s| s.len()) {
348 let rest = &trimmed[trimmed.len() - suffix_len..];
350 Some(rest.trim().to_string())
351 } else if let Some(suffix_len) = lower.strip_prefix("bearer").map(|s| s.len()) {
352 let rest = &trimmed[trimmed.len() - suffix_len..];
354 Some(rest.trim().to_string())
355 } else {
356 Some(trimmed.to_string())
358 }
359}
360
361pub fn extract_route_uri(req: &Request) -> String {
370 req.uri().path().to_string()
371}
372
373pub fn is_route_allowed(route_uri: &str, allow_list: &[String]) -> bool {
381 for pattern in allow_list {
382 if pattern == route_uri {
383 return true;
384 }
385 if pattern.contains('*') && wildcard_match(pattern, route_uri) {
386 return true;
387 }
388 }
389 false
390}
391
392pub fn wildcard_match(pattern: &str, text: &str) -> bool {
398 simple_wildcard_match(pattern, text)
399}
400
401fn simple_wildcard_match(pattern: &str, text: &str) -> bool {
405 let p: Vec<char> = pattern.chars().collect();
406 let t: Vec<char> = text.chars().collect();
407 let m = p.len();
408 let n = t.len();
409
410 let mut dp = vec![vec![false; n + 1]; m + 1];
412 dp[0][0] = true;
413
414 for i in 1..=m {
416 if p[i - 1] == '*' {
417 dp[i][0] = dp[i - 1][0];
418 }
419 }
420
421 for i in 1..=m {
422 for j in 1..=n {
423 if p[i - 1] == '*' {
424 dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
426 } else if p[i - 1] == t[j - 1] {
427 dp[i][j] = dp[i - 1][j - 1];
428 }
429 }
430 }
431
432 dp[m][n]
433}
434
435#[tracing::instrument(skip(claims))]
447pub fn verify_issuer(claims: &JwtClaims, expected_issuer: &str) -> bool {
448 match &claims.iss {
449 Some(iss) => iss == expected_issuer,
450 None => false,
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use axum::body::Body;
458 use axum::http::StatusCode;
459 use axum::Router;
460 use http_body_util::BodyExt;
461 use tower::ServiceExt;
462
463 async fn read_body(resp: Response) -> String {
468 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
469 String::from_utf8(bytes.to_vec()).unwrap()
470 }
471
472 fn make_request_with_uri(method: &str, uri: &str) -> Request {
473 Request::builder()
474 .method(method)
475 .uri(uri)
476 .body(Body::empty())
477 .unwrap()
478 }
479
480 fn make_request_with_auth(method: &str, uri: &str, auth: &str) -> Request {
481 Request::builder()
482 .method(method)
483 .uri(uri)
484 .header("Authorization", auth)
485 .body(Body::empty())
486 .unwrap()
487 }
488
489 fn make_test_token(secret: &str, issuer: &str, user_id: i64, exp_offset_secs: i64) -> String {
491 let encoder = JwtEncoder::new(secret);
492 let now = std::time::SystemTime::now()
493 .duration_since(std::time::UNIX_EPOCH)
494 .unwrap()
495 .as_secs() as i64;
496 let claims = JwtClaims::new("test_user", now + exp_offset_secs)
497 .with_issuer(issuer)
498 .with_user_id(user_id);
499 encoder.encode(&claims).expect("encode token")
500 }
501
502 #[test]
507 fn test_extract_token_from_header_with_bearer_prefix() {
508 let token = extract_token_from_header("Bearer abc123");
510 assert_eq!(token, Some("abc123".to_string()));
511 }
512
513 #[test]
514 fn test_extract_token_from_header_with_lowercase_bearer() {
515 let token = extract_token_from_header("bearer abc123");
517 assert_eq!(token, Some("abc123".to_string()));
518 }
519
520 #[test]
521 fn test_extract_token_from_header_with_uppercase_bearer() {
522 let token = extract_token_from_header("BEARER abc123");
524 assert_eq!(token, Some("abc123".to_string()));
525 }
526
527 #[test]
528 fn test_extract_token_from_header_without_bearer_prefix() {
529 let token = extract_token_from_header("abc123");
531 assert_eq!(token, Some("abc123".to_string()));
532 }
533
534 #[test]
535 fn test_extract_token_from_header_with_empty_string() {
536 let token = extract_token_from_header("");
537 assert_eq!(token, None);
538 }
539
540 #[test]
541 fn test_extract_token_from_header_with_only_whitespace() {
542 let token = extract_token_from_header(" ");
543 assert_eq!(token, None);
544 }
545
546 #[test]
547 fn test_extract_token_from_header_with_bearer_no_space() {
548 let token = extract_token_from_header("bearerabc");
550 assert_eq!(token, Some("abc".to_string()));
551 }
552
553 #[test]
554 fn test_extract_token_from_header_trims_whitespace() {
555 let token = extract_token_from_header(" Bearer abc123 ");
557 assert_eq!(token, Some("abc123".to_string()));
558 }
559
560 #[test]
565 fn test_is_route_allowed_exact_match() {
566 let allow = vec!["/passport/login".to_string()];
567 assert!(is_route_allowed("/passport/login", &allow));
568 assert!(!is_route_allowed("/passport/logout", &allow));
569 }
570
571 #[test]
572 fn test_is_route_allowed_multiple_entries() {
573 let allow = vec![
574 "/passport/login".to_string(),
575 "/task/task/userClerk".to_string(),
576 ];
577 assert!(is_route_allowed("/passport/login", &allow));
578 assert!(is_route_allowed("/task/task/userClerk", &allow));
579 assert!(!is_route_allowed("/passport/logout", &allow));
580 }
581
582 #[test]
583 fn test_is_route_allowed_wildcard_suffix() {
584 let allow = vec!["/upload.library/*".to_string()];
586 assert!(is_route_allowed("/upload.library/any", &allow));
587 assert!(is_route_allowed("/upload.library/sub/deep", &allow));
588 assert!(!is_route_allowed("/upload.library", &allow)); assert!(!is_route_allowed("/other/path", &allow));
590 }
591
592 #[test]
593 fn test_is_route_allowed_empty_list() {
594 let allow: Vec<String> = vec![];
595 assert!(!is_route_allowed("/any/path", &allow));
596 }
597
598 #[test]
599 fn test_wildcard_match_plain() {
600 assert!(wildcard_match("/upload/*", "/upload/any"));
601 assert!(wildcard_match("/upload/*", "/upload/sub/deep"));
602 assert!(!wildcard_match("/upload/*", "/other/any"));
603 }
604
605 #[test]
606 fn test_wildcard_match_exact_no_star() {
607 assert!(wildcard_match("/passport/login", "/passport/login"));
609 assert!(!wildcard_match("/passport/login", "/passport/logout"));
610 }
611
612 #[test]
613 fn test_wildcard_match_multiple_stars() {
614 assert!(wildcard_match("/*/*", "/a/b"));
615 assert!(wildcard_match("/*/*", "/abc/def"));
616 assert!(!wildcard_match("/*/*", "/a"));
617 }
618
619 #[test]
620 fn test_wildcard_match_star_at_end() {
621 assert!(wildcard_match("/api/*", "/api/v1/users"));
622 assert!(wildcard_match("/api/*", "/api/"));
623 assert!(!wildcard_match("/api/*", "/api"));
624 }
625
626 #[test]
627 fn test_wildcard_match_empty_pattern_and_text() {
628 assert!(wildcard_match("", ""));
629 assert!(!wildcard_match("", "abc"));
630 assert!(!wildcard_match("abc", ""));
631 }
632
633 #[test]
634 fn test_wildcard_match_star_only() {
635 assert!(wildcard_match("*", ""));
637 assert!(wildcard_match("*", "anything"));
638 assert!(wildcard_match("*", "/path/to/anything"));
639 }
640
641 #[test]
646 fn test_verify_issuer_matches() {
647 let claims = JwtClaims::new("user", 9999999999).with_issuer("https://mall.ljclz.shop");
648 assert!(verify_issuer(&claims, "https://mall.ljclz.shop"));
649 }
650
651 #[test]
652 fn test_verify_issuer_mismatch() {
653 let claims = JwtClaims::new("user", 9999999999).with_issuer("https://evil.com");
654 assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
655 }
656
657 #[test]
658 fn test_verify_issuer_missing() {
659 let claims = JwtClaims::new("user", 9999999999);
661 assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
662 }
663
664 #[test]
669 fn test_auth_config_default_matches_php() {
670 let config = AuthConfig::default();
672 assert_eq!(config.secret, "shengzhuang");
673 assert_eq!(config.issuer, "https://mall.ljclz.shop");
674 assert_eq!(config.expiration, 3600 * 24 * 30);
675 assert_eq!(
677 config.allow_all_action,
678 vec![
679 "/passport/login".to_string(),
680 "/task/task/userClerk".to_string(),
681 ]
682 );
683 }
684
685 #[test]
686 fn test_auth_config_default_allow_all_action_constant() {
687 assert_eq!(DEFAULT_ALLOW_ALL_ACTION.len(), 2);
689 assert_eq!(DEFAULT_ALLOW_ALL_ACTION[0], "/passport/login");
690 assert_eq!(DEFAULT_ALLOW_ALL_ACTION[1], "/task/task/userClerk");
691 }
692
693 #[test]
694 fn test_auth_config_builder_methods() {
695 let config = AuthConfig::default()
696 .with_secret("custom-secret")
697 .with_issuer("https://custom.com")
698 .with_allow_all_action(vec!["/custom/login".to_string()]);
699
700 assert_eq!(config.secret, "custom-secret");
701 assert_eq!(config.issuer, "https://custom.com");
702 assert_eq!(config.allow_all_action, vec!["/custom/login".to_string()]);
703 }
704
705 #[test]
706 fn test_auth_default_constants_match_php() {
707 assert_eq!(DEFAULT_ISSUER, "https://mall.ljclz.shop");
709 assert_eq!(DEFAULT_SECRET, "shengzhuang");
710 assert_eq!(DEFAULT_EXPIRATION, 3600 * 24 * 30);
711 }
712
713 #[test]
718 fn test_extract_route_uri_strips_query_string() {
719 let req = Request::builder()
720 .uri("/passport/login?foo=bar&baz=qux")
721 .body(Body::empty())
722 .unwrap();
723 assert_eq!(extract_route_uri(&req), "/passport/login");
724 }
725
726 #[test]
727 fn test_extract_route_uri_no_query() {
728 let req = Request::builder()
729 .uri("/api/users")
730 .body(Body::empty())
731 .unwrap();
732 assert_eq!(extract_route_uri(&req), "/api/users");
733 }
734
735 #[test]
736 fn test_extract_route_uri_root() {
737 let req = Request::builder().uri("/").body(Body::empty()).unwrap();
738 assert_eq!(extract_route_uri(&req), "/");
739 }
740
741 fn build_app(config: AuthConfig) -> Router {
747 Router::new()
748 .route("/protected", axum::routing::get(|| async { "protected" }))
749 .route("/passport/login", axum::routing::get(|| async { "login" }))
750 .route(
751 "/upload.library/test",
752 axum::routing::get(|| async { "upload" }),
753 )
754 .layer(axum::middleware::from_fn_with_state(
755 config,
756 auth_middleware,
757 ))
758 }
759
760 #[tokio::test]
761 async fn test_auth_middleware_allows_whitelisted_route() {
762 let app = build_app(AuthConfig::default());
764 let resp = app
765 .oneshot(make_request_with_uri("GET", "/passport/login"))
766 .await
767 .unwrap();
768 assert_eq!(resp.status(), StatusCode::OK);
769 let body = read_body(resp).await;
770 assert_eq!(body, "login");
771 }
772
773 #[tokio::test]
774 async fn test_auth_middleware_rejects_missing_authorization_header() {
775 let app = build_app(AuthConfig::default());
777 let resp = app
778 .oneshot(make_request_with_uri("GET", "/protected"))
779 .await
780 .unwrap();
781 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
783 assert!(body.contains("\"code\":-1"));
784 assert!(body.contains("缺少必要的参数,请重新登陆!"));
785 }
786
787 #[tokio::test]
788 async fn test_auth_middleware_rejects_empty_authorization_header() {
789 let app = build_app(AuthConfig::default());
791 let resp = app
792 .oneshot(make_request_with_auth("GET", "/protected", ""))
793 .await
794 .unwrap();
795 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
796 let body = read_body(resp).await;
797 assert!(body.contains("\"code\":-1"));
798 }
799
800 #[tokio::test]
801 async fn test_auth_middleware_rejects_invalid_token() {
802 let app = build_app(AuthConfig::default());
804 let resp = app
805 .oneshot(make_request_with_auth(
806 "GET",
807 "/protected",
808 "invalid.token.here",
809 ))
810 .await
811 .unwrap();
812 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
813 let body = read_body(resp).await;
814 assert!(body.contains("\"code\":-1"));
815 assert!(body.contains("缺少必要的参数,请重新登陆!"));
816 }
817
818 #[tokio::test]
819 async fn test_auth_middleware_rejects_expired_token() {
820 let config = AuthConfig::default();
822 let token = make_test_token(&config.secret, &config.issuer, 1, -3600);
824 let app = build_app(config.clone());
825 let resp = app
826 .oneshot(make_request_with_auth("GET", "/protected", &token))
827 .await
828 .unwrap();
829 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
830 let body = read_body(resp).await;
831 assert!(body.contains("\"code\":-1"));
832 }
833
834 #[tokio::test]
835 async fn test_auth_middleware_rejects_wrong_secret_token() {
836 let config = AuthConfig::default();
838 let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
839 let app = build_app(config.clone());
840 let resp = app
841 .oneshot(make_request_with_auth("GET", "/protected", &token))
842 .await
843 .unwrap();
844 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
845 }
846
847 #[tokio::test]
848 async fn test_auth_middleware_rejects_wrong_issuer_token() {
849 let config = AuthConfig::default();
851 let token = make_test_token(&config.secret, "https://evil.com", 1, 3600);
852 let app = build_app(config.clone());
853 let resp = app
854 .oneshot(make_request_with_auth("GET", "/protected", &token))
855 .await
856 .unwrap();
857 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
858 let body = read_body(resp).await;
859 assert!(body.contains("缺少必要的参数,请重新登陆!"));
860 }
861
862 #[tokio::test]
863 async fn test_auth_middleware_rejects_token_without_user_id() {
864 let config = AuthConfig::default();
866 let encoder = JwtEncoder::new(&config.secret);
867 let now = std::time::SystemTime::now()
868 .duration_since(std::time::UNIX_EPOCH)
869 .unwrap()
870 .as_secs() as i64;
871 let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
873 let token = encoder.encode(&claims).unwrap();
874 let app = build_app(config.clone());
875 let resp = app
876 .oneshot(make_request_with_auth("GET", "/protected", &token))
877 .await
878 .unwrap();
879 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
880 let body = read_body(resp).await;
881 assert!(body.contains("\"code\":-1"));
882 assert!(body.contains("not_login"));
883 }
884
885 #[tokio::test]
886 async fn test_auth_middleware_rejects_token_with_zero_user_id() {
887 let config = AuthConfig::default();
889 let token = make_test_token(&config.secret, &config.issuer, 0, 3600);
890 let app = build_app(config.clone());
891 let resp = app
892 .oneshot(make_request_with_auth("GET", "/protected", &token))
893 .await
894 .unwrap();
895 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
896 let body = read_body(resp).await;
897 assert!(body.contains("not_login"));
898 }
899
900 #[tokio::test]
901 async fn test_auth_middleware_accepts_valid_token_with_bearer_prefix() {
902 let config = AuthConfig::default();
904 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
905 let app = build_app(config.clone());
906 let resp = app
907 .oneshot(make_request_with_auth(
908 "GET",
909 "/protected",
910 &format!("Bearer {}", token),
911 ))
912 .await
913 .unwrap();
914 assert_eq!(resp.status(), StatusCode::OK);
915 let body = read_body(resp).await;
916 assert_eq!(body, "protected");
917 }
918
919 #[tokio::test]
920 async fn test_auth_middleware_accepts_valid_token_without_bearer_prefix() {
921 let config = AuthConfig::default();
923 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
924 let app = build_app(config.clone());
925 let resp = app
926 .oneshot(make_request_with_auth("GET", "/protected", &token))
927 .await
928 .unwrap();
929 assert_eq!(resp.status(), StatusCode::OK);
930 }
931
932 #[tokio::test]
933 async fn test_auth_middleware_accepts_lowercase_bearer_prefix() {
934 let config = AuthConfig::default();
936 let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
937 let app = build_app(config.clone());
938 let resp = app
939 .oneshot(make_request_with_auth(
940 "GET",
941 "/protected",
942 &format!("bearer {}", token),
943 ))
944 .await
945 .unwrap();
946 assert_eq!(resp.status(), StatusCode::OK);
947 }
948
949 #[tokio::test]
950 async fn test_auth_middleware_supports_wildcard_whitelist() {
951 let config =
953 AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
954 let app = build_app(config);
955 let resp = app
956 .oneshot(make_request_with_uri("GET", "/upload.library/test"))
957 .await
958 .unwrap();
959 assert_eq!(resp.status(), StatusCode::OK);
960 let body = read_body(resp).await;
961 assert_eq!(body, "upload");
962 }
963
964 #[tokio::test]
965 async fn test_auth_middleware_wildcard_does_not_overmatch() {
966 let config =
968 AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
969 let app = build_app(config);
970 let resp = app
972 .oneshot(make_request_with_uri("GET", "/protected"))
973 .await
974 .unwrap();
975 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
976 }
977
978 #[tokio::test]
979 async fn test_auth_middleware_injects_user_id_into_extensions() {
980 let config = AuthConfig::default();
982 let token = make_test_token(&config.secret, &config.issuer, 99, 3600);
983 let app = Router::new()
984 .route(
985 "/protected",
986 axum::routing::get(|req: Request| async move {
987 let user = req.extensions().get::<AuthenticatedUser>().unwrap();
988 format!("user_id:{}", user.user_id)
989 }),
990 )
991 .layer(axum::middleware::from_fn_with_state(
992 config.clone(),
993 auth_middleware,
994 ));
995 let resp = app
996 .oneshot(make_request_with_auth("GET", "/protected", &token))
997 .await
998 .unwrap();
999 assert_eq!(resp.status(), StatusCode::OK);
1000 let body = read_body(resp).await;
1001 assert_eq!(body, "user_id:99");
1002 }
1003
1004 #[tokio::test]
1005 async fn test_auth_middleware_returns_correct_error_code_for_missing_token() {
1006 let app = build_app(AuthConfig::default());
1008 let resp = app
1009 .oneshot(make_request_with_uri("GET", "/protected"))
1010 .await
1011 .unwrap();
1012 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = read_body(resp).await;
1014 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1016 assert_eq!(json["code"], -1);
1017 assert_eq!(json["msg"], "缺少必要的参数,请重新登陆!");
1018 assert_eq!(json["data"], serde_json::json!({}));
1019 }
1020
1021 #[tokio::test]
1022 async fn test_auth_middleware_returns_correct_error_code_for_not_login() {
1023 let config = AuthConfig::default();
1025 let encoder = JwtEncoder::new(&config.secret);
1026 let now = std::time::SystemTime::now()
1027 .duration_since(std::time::UNIX_EPOCH)
1028 .unwrap()
1029 .as_secs() as i64;
1030 let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
1031 let token = encoder.encode(&claims).unwrap();
1032 let app = build_app(config.clone());
1033 let resp = app
1034 .oneshot(make_request_with_auth("GET", "/protected", &token))
1035 .await
1036 .unwrap();
1037 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1038 let body = read_body(resp).await;
1039 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1040 assert_eq!(json["code"], -1);
1041 assert_eq!(json["msg"], "not_login");
1042 }
1043
1044 #[tokio::test]
1045 async fn test_auth_middleware_custom_secret_and_issuer() {
1046 let config = AuthConfig::default()
1048 .with_secret("custom-secret")
1049 .with_issuer("https://custom.com");
1050 let token = make_test_token("custom-secret", "https://custom.com", 1, 3600);
1051 let app = build_app(config);
1052 let resp = app
1053 .oneshot(make_request_with_auth("GET", "/protected", &token))
1054 .await
1055 .unwrap();
1056 assert_eq!(resp.status(), StatusCode::OK);
1057 }
1058
1059 #[tokio::test]
1060 async fn test_auth_middleware_rejects_token_signed_with_default_secret_when_custom_configured()
1061 {
1062 let config = AuthConfig::default().with_secret("custom-secret");
1064 let token = make_test_token("shengzhuang", &config.issuer, 1, 3600);
1065 let app = build_app(config);
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 }
1072
1073 #[tokio::test]
1074 async fn test_auth_middleware_preserves_query_string_in_route_match() {
1075 let app = build_app(AuthConfig::default());
1077 let resp = app
1078 .oneshot(make_request_with_uri(
1079 "GET",
1080 "/passport/login?redirect=/home",
1081 ))
1082 .await
1083 .unwrap();
1084 assert_eq!(resp.status(), StatusCode::OK);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_auth_middleware_handles_token_with_only_bearer_prefix() {
1089 let app = build_app(AuthConfig::default());
1091 let resp = app
1092 .oneshot(make_request_with_auth("GET", "/protected", "Bearer "))
1093 .await
1094 .unwrap();
1095 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1096 }
1097
1098 #[test]
1103 fn test_authorization_header_name_aligns_with_php() {
1104 let header_name = axum::http::header::AUTHORIZATION;
1108 assert_eq!(header_name.as_str(), "authorization");
1109 }
1111
1112 #[test]
1117 fn test_php_default_allow_all_action_matches_rust() {
1118 let php_allow = vec!["/passport/login", "/task/task/userClerk"];
1120 assert_eq!(php_allow, DEFAULT_ALLOW_ALL_ACTION);
1122 }
1123
1124 #[test]
1125 fn test_php_jwt_config_matches_rust() {
1126 let php_issuer = "https://mall.ljclz.shop";
1128 let php_secret = "shengzhuang";
1129 let php_expire = 3600 * 24 * 30;
1130
1131 assert_eq!(php_issuer, DEFAULT_ISSUER);
1133 assert_eq!(php_secret, DEFAULT_SECRET);
1134 assert_eq!(php_expire, DEFAULT_EXPIRATION);
1135 }
1136}