// jjwt - gatewayμ λμΌν λ²μ μ jjwt μ¬μ©
implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
jwt:
secret: ${JWT_SECRET}
access-token-expiration-ms: 86400000 # 24μκ°
# access-token-expiration-ms: 900000 # μ΄μμ 15λΆ
package com.pagely.userservice.infrastructure.security;
/**
* JWT κ΄λ ¨ μ€μ λ§€ν.
*
* @param secret JWT μλͺ
μ© λΉλ°ν€ (HS256, 32λ°μ΄νΈ μ΄μ)
* @param accessTokenExpirationMs AccessToken λ§λ£ μκ° (λ°λ¦¬μ΄)
*/
@ConfigurationProperties(prefix = "jwt") // ServiceApplication - @EnableConfigurationProperties(JwtProperties.class)
public record JwtProperties(
String secret,
long accessTokenExpirationMs
) {
}
@Component
public class JwtTokenProvider {
private static final String CLAIM_ROLE = "role";
private final SecretKey secretKey;
private final long accessTokenExpirationMs;
public JwtTokenProvider(JwtProperties jwtProperties) {
this.secretKey = Keys.hmacShaKeyFor(
jwtProperties.secret().getBytes(StandardCharsets.UTF_8)
);
this.accessTokenExpirationMs = jwtProperties.accessTokenExpirationMs();
}
/**
* AccessToken μμ±.
*
* @param userId μ μ UUID (sub ν΄λ μ)
* @param role μ μ κΆν λ¬Έμμ΄ (role ν΄λ μ)
* @return JWT ν ν° λ¬Έμμ΄
*/
public String createAccessToken(UUID userId, String role) {
long now = System.currentTimeMillis();
Date issuedAt = new Date(now);
Date expiration = new Date(now + accessTokenExpirationMs);
return Jwts.builder()
.subject(userId.toString())
.claim(CLAIM_ROLE, role)
.issuedAt(issuedAt)
.expiration(expiration)
.signWith(secretKey)
.compact();
}
/**
* λ§λ£ μκ°(μ΄ λ¨μ) λ°ν. μλ΅ DTO μ expiresIn μ μ¬μ©.
*/
public long getAccessTokenExpirationSeconds() {
return accessTokenExpirationMs / 1000;
}
}
/**
* ν ν° λ°κΈ μλ΅ DTO.
*
* <p>OAuth 2.0 RFC 6749 μ token endpoint μλ΅ νμ μ°Έμ‘°.</p>
*
* @param accessToken λ°κΈλ JWT
* @param tokenType ν ν° νμ
(νμ "Bearer")
* @param expiresIn λ§λ£κΉμ§ λ¨μ μκ° (μ΄)
*/
public record TokenResponse(
String accessToken,
String tokenType,
long expiresIn
) {
public static TokenResponse of(String accessToken, long expiresInSeconds) {
return new TokenResponse(accessToken, "Bearer", expiresInSeconds);
}
}
π OAuth 2.0 νμ€ νμ
RFC 6749 Section 5.1:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600
}
νμ€μ λ°λ₯΄λ©΄
ν΄λΌμ΄μΈνΈ λΌμ΄λΈλ¬λ¦¬ νΈνμ΄ μ½λ€. (axios interceptor λ± μλ μΈμ)
μΆν RefreshToken μΆκ° μ OAuth 2.0λ‘ μ½κ² νμ₯ν μ μλ€.
νμ€μ snake_case(access_token) μ΄μ§λ§,
μλΉμ€ λ΄ μΌκ΄μ±μ μ°μ ν΄μ camelCaseλ₯Ό(accessToken) μ¬μ©νκΈ°λ‘ νλ€.
μΈλΆ OAuth νΈν νμ μ @JsonProperty λ‘ λ³ννλ©΄ λλ€κ³ νλ€.
/**
* λ‘κ·ΈμΈ μμ² DTO.
*
* <p>POST /api/v1/auth/token μ Request Body.</p>
*/
public record LoginRequest(
@NotBlank(message = "λ‘κ·ΈμΈ μμ΄λλ νμμ
λλ€.")
String loginId,
@NotBlank(message = "λΉλ°λ²νΈλ νμμ
λλ€.")
String password
) {
public LoginCommand toCommand() {
return new LoginCommand(
loginId,
password
);
}
}
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthApplicationService authApplicationService;
@PostMapping("/token")
public ResponseEntity<ApiResponse> login(@Valid @RequestBody LoginRequest request) {
TokenResponse response = authApplicationService.login(request.toCommand());
return ApiResponse.ok(response);
}
}



κ° μ€λ λλ§λ€ μκΈ°λ§μ λ 립λ λ³μ μ μ₯μ.
private static String shared = "hello";
// μ€λ λ A κ° "world" λ‘ λ³κ²½νλ©΄
// μ€λ λ B λ "world" λ‘ λ³΄μ β λμμ± λ¬Έμ
public static final ThreadLocal<UserContext> CONTEXT;
ββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ
β Thread A β β Thread B β
ββββββββββββββββββββββββββββββββββββ€ ββββββββββββββββββββββββββββββββββββ€
β ββββββββββββββββββββββββββββββββ β β ββββββββββββββββββββββββββββββββ β
β β ThreadLocalMap β β β β ThreadLocalMap β β
β ββββββββββββββββ¬ββββββββββββββββ€ β β ββββββββββββββββ¬ββββββββββββββββ€ β
β β Key β Value β β β β Key β Value β β
β ββββββββββββββββΌββββββββββββββββ€ β β ββββββββββββββββΌββββββββββββββββ€ β
β β CONTEXT β userA β β β β CONTEXT β userB β β
β ββββββββββββββββ΄ββββββββββββββββ β β ββββββββββββββββ΄ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ
(A,B μλ‘ κ°μ λΆκ°)
μλ리μ€: Tomcat κ°μ μ€λ λ ν νκ²½
Tomcat μ μ컀 μ€λ λ ν: 200κ° μ€λ λ
[μμ² 1]
μ€λ λ A λ°μ
CONTEXT.set(userA) β Thread A μ Map μ μ μ₯
... μ²λ¦¬ ...
μλ΅ λ°ν
β μ΄ μμ μ CLEAR νμ§ μμΌλ©΄ ?
[μμ² 2 (λ€λ₯Έ μ¬μ©μ)]
μ€λ λ A λ€μ λ°μ (μ€λ λ νμμ μ¬μ¬μ©)
CONTEXT.get() β userA κ·Έλλ‘ λ¨μμμ! β
β λ€λ₯Έ μ¬μ©μ μμ²μ μ²λ¦¬νλ©΄μ μ΄μ μ¬μ©μ μ 보 μ¬μ©
β **κΆν λμ / 보μ μ¬κ³ **
κ·Έλμ try-finally + clear()
try {
UserContextHolder.set(...);
chain.doFilter(...);
} finally {
UserContextHolder.clear(); // νμ
}
β λΉλκΈ° μμ μ 컨ν μ€νΈ μ λμ΄κ°
public void method() {
UserContextHolder.set(userA);
CompletableFuture.supplyAsync(() -> {
// λ³λ μ€λ λμμ μ€ν
return UserContextHolder.get(); // null!
});
}
μ΄μ : ThreadLocal μ νμ¬ μ€λ λλ§ μΈμ. λ€λ₯Έ μ€λ λλ μκΈ° Map λ§ λ΄.
ν΄κ²°λ²
InheritableThreadLocal β μμ μ€λ λ μμ± μ λ³΅μ¬ (κ·Όλ³Έ ν΄κ²° μλ)TaskDecorator β Spring μ λΉλκΈ° μμ
μ λͺ
μμ μΌλ‘ 컨ν
μ€νΈ 볡μ¬MDC (SLF4J) β λ‘κΉ
μ νν΄ λΉμ·ν ν¨ν΄μλ μ λ¬ β λλ€ μΊ‘μ²β MVP μμ λκΈ° μ²λ¦¬μμλ§ μ¬μ© κ³ λ €.


@GetMapping("/me/_debug")
@AuthRequired
public ResponseEntity<ApiResponse> debugMe(
@CurrentUserId UUID userId,
@CurrentUserRole Role role
) {
return ApiResponse.ok(Map.of(
"userId", userId.toString(),
"role", role.name()
));
}


