소스 정보
- 저장소
- puk0806/gugbab-claude
- 최근 소스 활동
- 2026년 6월 23일 04:41
- 감지된 SKILL.md 언어
- 한국어
- 스타
- 2
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/puk0806/gugbab-claude --skill jwt-auth명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
SOC 직업 분류 기준
| 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 정의