with one click
jwt-auth
Rust JWT 인증 패턴 - jsonwebtoken 크레이트 + Axum 미들웨어 기반 토큰 생성/검증/인증 처리
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Rust JWT 인증 패턴 - jsonwebtoken 크레이트 + Axum 미들웨어 기반 토큰 생성/검증/인증 처리
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
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 정의