인증 / 인가 처리

JWT 2단계 인증 플로우, 토큰 저장 규칙, 에러 코드

마지막 수정: 2026-05

인증 / 인가 처리

JWT 2단계 인증 플로우

인증은 아래 2단계로 구성된다:

1단계: API 인증 획득

POST /auth/authorize

Request:

{
    "clientId": "...",
    "clientSecret": "...",
    "grantType": "client_credentials"
}

Response:

{
    "accessToken": "eyJhbGc...",
    "refreshToken": "eyJhbGc...",
    "tokenType": "Bearer",
    "expiresIn": 1800
}

2단계: 사용자 로그인

POST /login/login
Authorization: Bearer {accessToken}

Request:

{
    "userId": "user01",
    "userPassword": "...",
    "deviceId": "device-uuid",
    "deviceNm": "Chrome/Windows"
}

Response:

{
    "accessToken": "eyJhbGc...",
    "refreshToken": "eyJhbGc...",
    "tokenType": "Bearer",
    "expiresIn": 1800
}

토큰 관련 API

기능메서드URL
API 인증 획득POST/auth/authorize
사용자 로그인POST/login/login
토큰 갱신POST/auth/refresh
로그아웃POST/login/logout
전체 디바이스 로그아웃POST/login/logout-all

토큰 저장 규칙

토큰저장 위치이유
Access Token메모리 (JS 변수)localStorage 저장 금지 (XSS 취약)
Refresh TokenHttpOnly CookieJavaScript 접근 차단
  • Access Token 만료: 30분
  • Refresh Token 만료: 7일
// Access Token 메모리 저장 (올바른 예)
let accessToken = null;

function setAccessToken(token) {
    accessToken = token;  // 메모리에만 저장
}

// 잘못된 예 — localStorage 저장 금지
localStorage.setItem('accessToken', token);  // 금지!

API 요청 시 토큰 전달

// Authorization 헤더에 Bearer 토큰 포함
fetch('/api/user/getList', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`
    },
    body: JSON.stringify(vo)
});

에러 코드 테이블

코드설명
LOGIN_FAILED아이디 또는 비밀번호 불일치
PWD_WARN_30DAYS비밀번호 30일 경과 경고
PWD_EXPIRED_90DAYS비밀번호 90일 경과 만료
ACCOUNT_DORMANT휴면 계정
ACCOUNT_LOCKED계정 잠김
INVALID_REFRESH_TOKEN유효하지 않은 리프레시 토큰
INVALID_CLIENT_CREDENTIALS잘못된 클라이언트 정보

에러 응답 예시:

{
    "success": false,
    "errorCode": "LOGIN_FAILED",
    "message": "아이디 또는 비밀번호가 올바르지 않습니다."
}

서버 측 토큰 검증

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenService jwtTokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = extractToken(request);

        if (token != null && jwtTokenService.validateToken(token)) {
            Authentication auth = jwtTokenService.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

인증 실패 로깅

// 인증/인가 실패 로그 반드시 기록 (사용자 식별자 포함, 개인정보 제외)
log.warn("로그인 실패: userId={}, errorCode={}", userId, errorCode);

// 개인정보(비밀번호 등) 로그 출력 금지
log.warn("로그인 실패: userId={}", userId);  // 올바른 예
log.warn("로그인 실패: password={}", password);  // 절대 금지

보안 체크리스트

  • [ ] Access Token: 메모리에만 저장 (localStorage 금지)
  • [ ] Refresh Token: HttpOnly Cookie 사용
  • [ ] 모든 API 요청: Authorization 헤더에 Bearer 토큰 포함
  • [ ] 토큰 만료 처리: 자동 갱신 또는 재로그인 유도
  • [ ] 인증/인가 실패 로그 기록 (개인정보 제외)
  • [ ] ANTHROPIC_API_KEY 등 서버 키는 환경변수로 관리