웹 애플리케이션을 개발할 때 "현재 로그인한 사용자의 ID"를 가져오는 작업은 필수적입니다. 하지만 매번 프론트엔드에서 ID를 파라미터로 받거나, 복잡한 시큐리티 로직을 컨트롤러에 적는 것은 보안과 생산성 측면에서 좋지 않습니다.
이 문제를 깔끔하게 해결해 주는 **@AuthenticationPrincipal**에 대해 알아보겠습니다.
1. 왜 @AuthenticationPrincipal인가?
❌ 기존 방식의 문제점 (ID를 파라미터로 받을 때)
- 보안 취약성: 사용자가 브라우저 개발자 도구에서 userId 파라미터를 조작하여 타인의 데이터를 조회/삭제하는 ID 변조 공격에 노출됩니다.
- 반복되는 검증: 서버는 파라미터로 온 ID가 진짜 로그인한 본인의 ID인지 확인하는 로직을 모든 API마다 작성해야 합니다.
- 지저분한 코드: 컨트롤러가 비즈니스 로직 외에 인증 정보를 파헤치는 코드들로 가득 차게 됩니다.
✅ @AuthenticationPrincipal의 장점
- 신뢰성: 프론트엔드가 보내주는 값이 아닌, 서버가 발행한 **암호화된 토큰(JWT)**에서 ID를 직접 꺼내오므로 조작이 불가능합니다.
- 간결함: 어노테이션 하나로 컨트롤러 파라미터에 사용자 정보를 즉시 주입받을 수 있습니다.
- 프론트엔드 편의성: 프론트엔드는 API 호출 시 userId를 일일이 챙길 필요 없이, HTTP 헤더에 토큰만 실어 보내면 됩니다.
2. JWT 환경에서의 동작 원리
JWT와 함께 사용할 때 전체적인 흐름은 다음과 같습니다.
- Request: 프론트엔드에서 HTTP Header에 JWT를 담아 요청을 보냅니다.
- Filter: JwtAuthenticationFilter가 토큰을 가로채서 유효성을 검사합니다.
- Extraction: 토큰 내부에 저장된 userId를 꺼냅니다.
- SecurityContext: 꺼낸 ID를 Authentication 객체에 담아 SecurityContextHolder에 저장합니다.
- Injection: 컨트롤러 실행 시 @AuthenticationPrincipal이 해당 값을 찾아 파라미터에 꽂아줍니다.
3. 실전 구현 단계 (Code-Level)
Step 1: JWT에 사용자 ID 포함하기 (Token 생성)
토큰을 만들 때 사용자 식별자(PK)를 Claim에 저장해야 합니다.
Java
public String createToken(Long userId, String email) {
Claims claims = Jwts.claims().setSubject(email);
claims.put("userId", userId); // 핵심: 토큰에 userId를 저장
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
Step 2: JWT 필터에서 Authentication 설정
토큰을 검증한 후, 컨트롤러에서 바로 꺼낼 수 있도록 인증 객체를 만들어 보관합니다.
Java
// JwtAuthenticationFilter 내부 로직 예시
if (token != null && jwtProvider.validateToken(token)) {
Long userId = jwtProvider.getUserId(token); // 토큰에서 ID 추출
// Principal 자리에 userId(Long)를 직접 넣거나, CustomUserDetails 객체를 생성해 넣습니다.
Authentication auth = new UsernamePasswordAuthenticationToken(userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
}
Step 3: 컨트롤러에서 주입받기
이제 모든 준비가 끝났습니다. 컨트롤러에서는 단 한 줄로 유저 ID를 얻습니다.
Java
@RestController
@RequestMapping("/api/notifications")
public class NotificationController {
@GetMapping("/unread/count")
public ResponseEntity<Integer> getCount(@AuthenticationPrincipal Long userId) {
// userId는 시큐리티 필터가 토큰에서 꺼내준 신뢰할 수 있는 값입니다.
return ResponseEntity.ok(notificationService.getUnreadCount(userId));
}
}
4. 핵심 요약 및 주의사항
- 용도 구분: "내 데이터"를 다룰 때는 @AuthenticationPrincipal을 사용하고, "검색 대상"이나 "다른 유저"를 지정할 때는 기존처럼 @PathVariable이나 @RequestParam을 사용하세요.
- 타입 매칭: 필터에서 Authentication 객체를 만들 때 principal 자리에 넣은 데이터 타입(예: Long, String, UserDetails)과 컨트롤러에서 받는 파라미터 타입이 일치해야 합니다.
- Null 체크: 로그인 권한이 필요 없는 API에서도 해당 어노테이션을 사용할 경우, 비로그인 시 null이 들어올 수 있으므로 적절한 처리가 필요합니다.
정리하자면, @AuthenticationPrincipal은 단순히 코드를 짧게 만드는 도구가 아니라, 서버 지향적인 인증 처리를 통해 보안을 강화하고 서버-클라이언트 간의 역할을 명확히 나누는 핵심 기술입니다.
'Archive(완료된 내용) > 포트폴리오 강화' 카테고리의 다른 글
| [Stock101] 프로젝트 기획 마무리 정리 - 10일차 (0) | 2026.02.03 |
|---|---|
| [Stock101] 프로젝트를 정리하자.- 9일차 (0) | 2026.02.01 |
| [Stock101] 프론트를 재정비하자.- 8일차 (1) | 2026.01.29 |
| [stock101] 프론트앤드 구조를 잘 만들어야 관리가 쉽다. (0) | 2026.01.29 |
| [Stock101] 반기 / 분기 공시 요약 및 전망 AI 기능 마무리- 8일차 (0) | 2026.01.28 |