인증 / 인가 처리
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 Token | HttpOnly Cookie | JavaScript 접근 차단 |
- 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 등 서버 키는 환경변수로 관리