Files
ga-commission-system/ga-common/src/main/java/com/ga/common/auth/JwtTokenProvider.java
T
GA Pro 17ab1098ec feat: 1200%룰 이연 분급 도래월 지급(PayInstallmentStep) + 코드베이스 신입교육 주석
PayInstallmentStep(Step 4.5): 1200%룰로 이연된 분급(installment_plan SCHEDULED)을
도래월(settle_month=정산월)에 recruit_ledger로 편입해 실제 지급. 이전엔 이연만 되고
도래월 지급 절차가 없어 차액이 영구 미지급되던 갭을 메움.
- BatchConfig job flow에 step4b(calcRecruit→payInstallment→calcMaintain) 등록
- InstallmentPlanMapper.resetPaidToScheduledByMonth(+XML)로 재실행 멱등
  (진입 시 PAID→SCHEDULED 역산, Step4 deleteBySettleMonth가 원장 정리)
- RecruitLedgerMapper insert에 reconcile_status 컬럼, SettlementContext.installmentPaidCount
- 단위테스트 3건(정상지급/멱등/계약미존재) GREEN

신입교육 주석: 컨트롤러/서비스/공통/프론트 전반에 도메인·코드 설명 주석 추가
(동작 변경 없음 — 빌드/테스트/타입체크 전부 GREEN으로 무회귀 확인).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:40:28 +09:00

110 lines
4.7 KiB
Java

package com.ga.common.auth;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* JWT(JSON Web Token)를 발급하고 검증하는 핵심 유틸 컴포넌트.
*
* <p>왜 필요한가: 로그인 성공 시 여기서 토큰을 만들어 클라이언트에 주고, 이후 요청마다
* {@link JwtAuthFilter}가 여기서 토큰을 검증한다. 즉 인증 토큰의 "생성"과 "해석"을 한곳에 모은 것.
*
* <p>토큰 종류: access(짧은 수명, 실제 API 호출용)와 refresh(긴 수명, access 재발급용) 두 가지.
* 서명 비밀키(secret)와 만료시간(ms)은 application 설정(ga.jwt.*)에서 주입받는다.
*/
@Slf4j
@Component
public class JwtTokenProvider {
private final SecretKey key; // HMAC 서명에 쓰는 비밀키(이 키로 서명/검증)
private final long accessExpire; // access 토큰 만료시간(ms)
private final long refreshExpire; // refresh 토큰 만료시간(ms)
// 생성자 주입: 설정값(@Value)이 없으면 기본값(access 2시간, refresh 7일) 사용.
public JwtTokenProvider(
@Value("${ga.jwt.secret}") String secret,
@Value("${ga.jwt.access-token-expire:7200000}") long accessExpire,
@Value("${ga.jwt.refresh-token-expire:604800000}") long refreshExpire
) {
byte[] raw = secret.getBytes(StandardCharsets.UTF_8);
// HMAC-SHA256은 키 길이가 최소 256비트(32바이트)여야 한다. 설정값이 짧으면 0으로 패딩해 길이를 맞춘다.
if (raw.length < 32) {
byte[] padded = new byte[32];
System.arraycopy(raw, 0, padded, 0, raw.length);
raw = padded;
}
this.key = Keys.hmacShaKeyFor(raw);
this.accessExpire = accessExpire;
this.refreshExpire = refreshExpire;
}
/** 로그인 시 발급하는 access 토큰. 사용자 ID/로그인ID/역할을 담는다. */
public String createAccessToken(Long userId, String loginId, List<String> roles) {
return create(userId, loginId, roles, accessExpire, "ACCESS");
}
/** access 만료 시 재발급에 쓰는 refresh 토큰. 역할은 담지 않고 수명이 길다. */
public String createRefreshToken(Long userId, String loginId) {
return create(userId, loginId, List.of(), refreshExpire, "REFRESH");
}
// 토큰 생성 공통 로직: subject=로그인ID, claims에 uid/roles/type, 발급/만료 시각을 넣고 비밀키로 서명.
private String create(Long userId, String loginId, List<String> roles, long expireMs, String type) {
Date now = new Date();
return Jwts.builder()
.subject(loginId)
.claims(Map.of(
"uid", userId,
"roles", roles,
"type", type
))
.issuedAt(now)
.expiration(new Date(now.getTime() + expireMs))
.signWith(key)
.compact();
}
/**
* 토큰을 비밀키로 검증하고 안의 claims(uid, roles 등)를 꺼낸다.
* 만료/위조 시 일반 예외 대신 업무 예외(BizException)로 바꿔 던져, 상위에서 표준 에러응답으로 처리하게 한다.
*/
public Claims parse(String token) {
try {
return Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token).getPayload();
} catch (ExpiredJwtException e) {
throw new BizException(ErrorCode.TOKEN_EXPIRED); // 만료된 토큰
} catch (JwtException | IllegalArgumentException e) {
throw new BizException(ErrorCode.TOKEN_INVALID); // 서명 불일치/형식 오류 등
}
}
/** 예외를 던지지 않고 true/false 로만 유효성을 확인하고 싶을 때 사용하는 편의 메서드. */
public boolean isValid(String token) {
try {
parse(token);
return true;
} catch (Exception e) {
return false;
}
}
// 만료시간 조회용 getter. 로그인 응답에 "토큰 만료까지 남은 시간"을 함께 내려줄 때 등에 쓰인다.
public long getAccessExpire() { return accessExpire; }
public long getRefreshExpire() { return refreshExpire; }
}