بنقرة واحدة
jwt-auth
Rust JWT 인증 패턴 - jsonwebtoken 크레이트 + Axum 미들웨어 기반 토큰 생성/검증/인증 처리
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Rust JWT 인증 패턴 - jsonwebtoken 크레이트 + Axum 미들웨어 기반 토큰 생성/검증/인증 처리
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Spring Security 5.5.x + jjwt 0.10.7 레거시 JWT 인증 - WebSecurityConfigurerAdapter, OncePerRequestFilter, javax.servlet 환경
Spring Boot 3.x + Spring Security 6.x + jjwt 0.12.x 기반 모던 JWT 인증 패턴. SecurityFilterChain Bean, 람다 DSL, jakarta.servlet, Virtual Threads 적용
Unity 6 LTS 2D 모바일 게임용 uGUI 시스템 전문 스킬. Canvas/RectTransform/TextMeshPro, 모바일 UI 패턴(팝업·무한 스크롤·광고·IAP), 성능 최적화, UI Toolkit과의 선택 기준 포함.
아크라시아(akrasia, 자제력 없음) 학술 논쟁의 핵심 구도와 주요 연구자·문헌을 빠르게 파악할 수 있는 도메인 지식 스킬. 도덕윤리교육 전공 대학원생(석/박사)이 학위논문·KCI 투고·세미나 준비 시 고대–현대–한국 학계–도덕심리학 흐름을 한 번에 짚도록 구성. <example>사용자: "아리스토텔레스의 propeteia와 astheneia 구분을 인용하려는데 출처를 알려줘"</example> <example>사용자: "데이비슨이 의지박약을 어떻게 가능하다고 봤는지 핵심 논증을 정리해줘"</example> <example>사용자: "한국 도덕교육 학계에서 아크라시아 다룬 논문 있어?"</example>
아리스토텔레스 『니코마코스 윤리학』에서 akrasia(자제력없음)와 akolasia(무절제)의 5축 차이를 정밀하게 정리한 학위논문 자료 스킬. NE VII.4 1147b20-1148b14, VII.8 1150b29-1151a28, III.10-12 1117b23-1119b18 절별 분해와 표준 학자 해석(Bostock, Broadie-Rowe, Pakaluk, Hursthouse, Charles 등)을 포함. 도덕교육 적용을 위한 두 상태 차이의 함의 및 한국어 번역어 처리 권장안 제공. <example>사용자: "akrates와 akolastos를 prohairesis 측면에서 어떻게 구분해야 하나요?"</example> <example>사용자: "NE VII.4의 ἁπλῶς akrasia가 akolasia와 어떻게 갈라지는지 절별 분해해주세요"</example> <example>사용자: "Hursthouse의 연속체 모델을 도덕교육 적용 절에서 어떻게 활용할 수 있나요?"</example>
한국 위기 대응 자원(자살·자해·정신건강·여성·청소년·노인·다문화) 핫라인과 앱·챗봇 안전 가드 응답 패턴 종합. 꿈 해몽·정신건강 앱 등 자가 진단/감정 콘텐츠 도메인에서 위험 신호 포착 시 안전한 자원 안내 문구를 작성할 때 참조. <example>사용자: "꿈 해몽 앱에 위기 안내 문구를 어떻게 넣을까?"</example> <example>사용자: "한국에서 자살예방 핫라인 번호가 어떻게 바뀌었지?"</example> <example>사용자: "정신건강 챗봇 안전 가드 응답 템플릿을 짜줘"</example>
| name | jwt-auth |
| description | Rust JWT 인증 패턴 - jsonwebtoken 크레이트 + Axum 미들웨어 기반 토큰 생성/검증/인증 처리 |
소스: https://docs.rs/jsonwebtoken/latest/jsonwebtoken/ | https://github.com/Keats/jsonwebtoken 소스: https://docs.rs/axum/latest/axum/middleware/ 검증일: 2026-06-20
주의: 이 문서는 jsonwebtoken 10.x / axum 0.8.x 기준으로 작성되었습니다. 버전 변경 시 API 시그니처와 기본값이 달라질 수 있으므로 공식 docs.rs를 반드시 확인하세요.
주의: jsonwebtoken 10.0부터 암호화 백엔드를 feature flag로 명시적으로 선택해야 합니다.
aws_lc_rs또는rust_crypto중 하나를 반드시 지정해야 합니다. 9.x에서 마이그레이션 시 Cargo.toml 변경이 필요합니다.
# Cargo.toml
[dependencies]
axum = "0.8"
axum-extra = { version = "0.10", features = ["typed-header"] }
# jsonwebtoken 10.x: 암호화 백엔드 feature를 반드시 선택해야 한다
# aws_lc_rs (권장: 성능 우수, AWS 환경에 최적화) 또는 rust_crypto (순수 Rust, 이식성 높음) 중 하나 선택
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["cors"] }
dotenvy = "0.15"
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String, // subject (사용자 ID 등)
pub exp: usize, // 만료 시간 (UNIX timestamp)
pub iat: usize, // 발급 시간
pub role: String, // 커스텀 필드 예시
}
exp는 UNIX timestamp (초 단위). chrono::Utc::now().timestamp() as usize 사용sub, exp는 JWT 표준 클레임. 커스텀 필드를 자유롭게 추가 가능use jsonwebtoken::{encode, EncodingKey, Header, Algorithm};
use chrono::Utc;
pub fn create_token(user_id: &str, role: &str, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now().timestamp() as usize;
let claims = Claims {
sub: user_id.to_string(),
exp: now + 3600, // 1시간 후 만료
iat: now,
role: role.to_string(),
};
// HS256 (대칭키)
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))
}
Header::default()는 HS256을 사용합니다.
use jsonwebtoken::{encode, EncodingKey, Header, Algorithm};
pub fn create_token_rsa(claims: &Claims, private_key_pem: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
let header = Header::new(Algorithm::RS256);
encode(&header, claims, &EncodingKey::from_rsa_pem(private_key_pem)?)
}
EncodingKey::from_rsa_pem() — PEM 형식 RSA 개인키EncodingKey::from_secret() — 바이트 슬라이스 (HS256/HS384/HS512)use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm, TokenData};
pub fn verify_token(token: &str, secret: &[u8]) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let validation = Validation::new(Algorithm::HS256);
// validate_exp: 기본 true — 만료된 토큰 자동 거부
// validate_nbf: 기본 false
// leeway: 기본 60초 — exp/nbf 판정 시 허용 오차
decode::<Claims>(token, &DecodingKey::from_secret(secret), &validation)
}
let mut validation = Validation::new(Algorithm::HS256);
validation.leeway = 0; // 허용 오차 제거
validation.validate_exp = true; // 만료 검증 (기본 true)
validation.validate_nbf = true; // nbf 검증 활성화
validation.set_issuer(&["my-app"]); // iss 검증
validation.set_audience(&["my-audience"]); // aud 검증
주의:
required_spec_claims의 기본값은 jsonwebtoken 버전에 따라 다를 수 있습니다. 9.x에서는 기본적으로exp가 포함됩니다. 정확한 기본값은 해당 버전의 docs.rs를 확인하세요.
pub fn verify_token_rsa(token: &str, public_key_pem: &[u8]) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let validation = Validation::new(Algorithm::RS256);
decode::<Claims>(token, &DecodingKey::from_rsa_pem(public_key_pem)?, &validation)
}
use jsonwebtoken::errors::ErrorKind;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
pub enum AuthError {
TokenExpired,
InvalidToken,
InvalidSignature,
MissingToken,
}
impl IntoResponse for AuthError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired"),
AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"),
AuthError::InvalidSignature => (StatusCode::UNAUTHORIZED, "Invalid signature"),
AuthError::MissingToken => (StatusCode::UNAUTHORIZED, "Missing authorization token"),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}
// jsonwebtoken 에러를 AuthError로 변환
impl From<jsonwebtoken::errors::Error> for AuthError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
match err.kind() {
ErrorKind::ExpiredSignature => AuthError::TokenExpired,
ErrorKind::InvalidSignature => AuthError::InvalidSignature,
ErrorKind::InvalidToken => AuthError::InvalidToken,
ErrorKind::InvalidAlgorithm => AuthError::InvalidToken,
ErrorKind::InvalidIssuer => AuthError::InvalidToken,
ErrorKind::InvalidAudience => AuthError::InvalidToken,
_ => AuthError::InvalidToken,
}
}
}
주요 ErrorKind 변형:
ExpiredSignature — 토큰 만료InvalidSignature — 서명 불일치InvalidToken — 토큰 형식 오류InvalidAlgorithm — 알고리즘 불일치InvalidIssuer / InvalidAudience / InvalidSubject — 클레임 불일치ImmatureSignature — nbf 이전 사용 시도use axum::{
extract::Request,
http::header::AUTHORIZATION,
middleware::Next,
response::Response,
Extension,
};
pub async fn auth_middleware(
mut req: Request,
next: Next,
) -> Result<Response, AuthError> {
// Authorization 헤더에서 Bearer 토큰 추출
let token = req
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AuthError::MissingToken)?;
// 환경변수에서 SECRET 로드
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
// 토큰 검증
let token_data = verify_token(token, secret.as_bytes())?;
// Claims를 Extension으로 주입 — 핸들러에서 꺼내 쓸 수 있음
req.extensions_mut().insert(token_data.claims);
Ok(next.run(req).await)
}
use axum::extract::State;
#[derive(Clone)]
pub struct AppState {
pub jwt_secret: String,
}
pub async fn auth_middleware_with_state(
State(state): State<AppState>,
mut req: Request,
next: Next,
) -> Result<Response, AuthError> {
let token = req
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AuthError::MissingToken)?;
let token_data = verify_token(token, state.jwt_secret.as_bytes())?;
req.extensions_mut().insert(token_data.claims);
Ok(next.run(req).await)
}
use axum::{routing::get, Router, middleware};
fn create_router(state: AppState) -> Router {
let protected = Router::new()
.route("/profile", get(profile_handler))
.route("/settings", get(settings_handler))
.layer(middleware::from_fn_with_state(state.clone(), auth_middleware_with_state));
let public = Router::new()
.route("/login", axum::routing::post(login_handler))
.route("/health", get(health_handler));
Router::new()
.merge(protected)
.merge(public)
.with_state(state)
}
핵심: .layer() 는 해당 Router에만 적용됩니다. 인증이 필요한 라우트와 공개 라우트를 분리하여 merge합니다.
use axum::Extension;
async fn profile_handler(
Extension(claims): Extension<Claims>,
) -> impl IntoResponse {
Json(serde_json::json!({
"user_id": claims.sub,
"role": claims.role,
}))
}
req.extensions_mut().insert(claims) 로 주입한 값을 Extension<Claims>로 추출Clone derive 필요#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RefreshClaims {
pub sub: String,
pub exp: usize,
pub iat: usize,
pub token_type: String, // "refresh"
}
pub fn create_refresh_token(user_id: &str, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now().timestamp() as usize;
let claims = RefreshClaims {
sub: user_id.to_string(),
exp: now + 60 * 60 * 24 * 7, // 7일
iat: now,
token_type: "refresh".to_string(),
};
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))
}
async fn refresh_handler(
State(state): State<AppState>,
Json(body): Json<RefreshRequest>,
) -> Result<Json<TokenResponse>, AuthError> {
// 1. Refresh Token 검증
let token_data = decode::<RefreshClaims>(
&body.refresh_token,
&DecodingKey::from_secret(state.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256),
)?;
// 2. token_type이 "refresh"인지 확인
if token_data.claims.token_type != "refresh" {
return Err(AuthError::InvalidToken);
}
// 3. 새 Access Token 발급
let access_token = create_token(&token_data.claims.sub, "user", state.jwt_secret.as_bytes())?;
Ok(Json(TokenResponse { access_token }))
}
Refresh Token 보안 고려사항:
// .env 파일
// JWT_SECRET=your-256-bit-secret-key-here
// JWT_REFRESH_SECRET=another-secret-for-refresh-tokens (선택)
use dotenvy::dotenv;
use std::env;
fn load_jwt_config() -> (String, String) {
dotenv().ok();
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let refresh_secret = env::var("JWT_REFRESH_SECRET").unwrap_or_else(|_| secret.clone());
(secret, refresh_secret)
}
.env 파일을 .gitignore에 반드시 추가jsonwebtoken 10.0.0에서 암호화 백엔드 선택이 필수가 되었다.
| 항목 | 9.x | 10.x |
|---|---|---|
| 암호화 백엔드 | 내장 (선택 불필요) | aws_lc_rs 또는 rust_crypto feature 명시 필수 |
| PEM 지원 | 기본 포함 | 기본 포함 (비활성화 가능: default-features = false) |
time 의존성 | time ^0.3 (Rust 1.84 이하 제약 있음) | 제거됨 (time 의존성 없음) |
| decode 함수 | Clone 바운드 있음 | Clone 바운드 제거 (10.2.0) |
# 9.x (이전)
jsonwebtoken = "9"
# 10.x (신규) — 백엔드 feature 선택 필수
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
# 또는
jsonwebtoken = { version = "10", features = ["aws_lc_rs"] }
주의:
aws_lc_rs와rust_crypto는 동시에 활성화할 수 없다. 둘 중 하나만 선택해야 한다.
// 9.x와 10.x에서 encode/decode API 시그니처는 동일
// 마이그레이션 시 코드 변경 없음 — Cargo.toml만 수정하면 됨
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))
decode::<Claims>(token, &DecodingKey::from_secret(secret), &validation)
src/
├── main.rs # 서버 실행, 라우터 구성
├── auth/
│ ├── mod.rs
│ ├── jwt.rs # encode/decode, Claims 정의
│ ├── middleware.rs # auth_middleware
│ └── error.rs # AuthError, IntoResponse 구현
├── handlers/
│ ├── mod.rs
│ ├── auth_handler.rs # login, refresh
│ └── protected.rs # 인증 필요 핸들러
└── state.rs # AppState 정의