커스텀 어노테이션이 필요한 상황
지금 구현하고 있는 프로젝트에서는 거의 대부분의 기능을 소셜 로그인 후 사용 가능하다. 따라서 거의 모든 API에서 JWT 토큰 인증 후 토큰에 있는 정보를 가지고 member의 정보(id, email 등등)을 추출하는 로직이 필요한데, 이를 컨트롤러에서 중복되는 코드를 계속 작성하기에는 너무 지저분하기도 하고, 깔끔하게 어노테이션 하나로 선언적으로 표현하고 싶어서 커스텀 어노테이션인 @AuthUser
를 만들기로 했다.
@AuthUser 어노테이션
@AuthUser는 여러 도메인에서 글로벌하게 사용할 어노테이션이므로, ./global/auth
아래에 만들어 주었다. 코드는 아래와 같다. (참고로 interface
앞의 @
는 오타가 아니라, 어노테이션 타입이라는 뜻이다.)
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Parameter(hidden = true)
/* 인증 객체 정보를 가지고 다니는 커스텀 어노테이션, AuthDetail의 필드인 member를 가져온다. */
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member")
public @interface AuthUser {
}
각 어노테이션들의 의미는 다음과 같다.
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
: @Target은 메타 어노테이션으로, 다른 어노테이션을 추가하는 데에만 사용된다. ElementType을 명시하면 해당 ElementType에서만 커스텀 어노테이션을 적용 가능하다. 여기서는 Controller의 파라미터로 받을 때 @AuthUser를 사용해야 하므로, ElementType을PARAMETER
로 명시했다. 그리고 그냥 어노테이션 자체로 사용하고 싶을 때가 있을 수 있으므로ANNOTATION_TYPE
도 같이 명시해주었다. 이 두 범위 이외에서 활용되면 에러가 발생한다.@Retention(RetentionPolicy.RUNTIME)
: 런타임에도 유지된다는(retention) 뜻이다. 3가지 타입이 있는데, 소스 코드 수준에서만 머물고 런타임에는 삭제되는SOURCE
,.class
파일에 기록되지만 런타임 중에는 삭제되는CLASS
(디폴트 설정), 런타임 중에 프로그램에서 엑세스 가능한RUNTIME
이 있다. 우리는 실제로 서비스에서 이 어노테이션을 사용해야 하므로RetentionPolicy.RUNTIME
으로 명시해야 한다.@Parameter(hidden = true)
: 필수는 아니지만, 나는 지금 Swagger UI를 사용해서 API 문서를 만들고 있는데, 이때@AuthUser
가 필수 파라미터로 표시되는 것을 막기 위해서hidden = true
옵션을 사용했다.
@AuthenticationPrincipal에 대해
내가 구현하고자 하는 어노테이션에서 가장 중요한 부분이다. 나는 JWT 토큰을 통해서 유저 인증이 완료되면 토큰에 일치하는 member 객체를 바로 받고 싶다. 이를 위해서 @AuthenticationPrincipal
어노테이션을 사용해야 한다. 이 어노테이션은 스프링 시큐리티의 AuthenticationPrincipalArgumentResolver
클래스를 활용해서 SecurityContextHolder
에 접근할 수 있다. 즉 인증 객체에 접근해서 값을 가져올 수 있는 것이다. 공식 문서에서는 다음과 같이 설명하고 있다.
Will resolve theCustomUser
argument usingAuthentication.getPrincipal()
from theSecurityContextHolder
. If theAuthentication
orAuthentication.getPrincipal()
isnull
, it will returnnull
.
@AuthenticationPrincipal
어노테이션은SecurityContextHolder
에서Authentication.getPrincipal()
을 통해서CustomUser
인수를 가져올 수 있습니다. 만약Authentication
또는Authentication.getPrincipal()
이null
이라면null
을 반환합니다.
@AuthenticationPrincipal
어노테이션 뒤에 붙는 (expression = "#this == 'anonymousUser' ? null : member")
은 SpEL(Spring Expression Language) 표현식인데, 만약 현재의 객체 #this
가 스프링 시큐리티에서 인증되지 않은 사용자를 나타내는 특별한 값인 anonymousUser
라면, null
을 반환하고, 그렇지 않을 경우에는 Authentication.getPrincipal()
를 통해서 가져올 수 있는 UserDetails
객체의 member
필드를 가져올 수 있다.
AuthDetails.java
나는 UserDetails
를 구현한 클래스로 AuthDetail
이 있다. 이 안에 내가 넣고 싶은 사용자 객체를 Member로 명시했기 때문에 @AuthenticationPrincipal
어노테이션이 member 객체를 가져올 수 있다.
@Data
@RequiredArgsConstructor
public class AuthDetails implements UserDetails {
/* 인증된 사용자에 대한 세부 정보를 다루는 UserDetails의 구현체(Principal 객체) */
private final Member member;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/* 맴버의 권한 반환 */
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(member.getMemberRole().name()));
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
/* 사용자의 식별자(일반적으로 유저명이나 이메일) */
return member.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Controller에서 사용하기
이제 커스텀 어노테이션을 만들었으므로 실제로 사용해주자. 아래와 같이 아무 API에서나 컨트롤러의 파라미터로 @AuthUser
를 명시하면, 내가 구성한 스프링 시큐리티의 로직대로 JWT 토큰 검증 후, 어노테이션에서 명시한 대로 인증이 완료되면 Member 객체를 반환한다. 그래서 Member 객체에서 id 필드 값을 바로 뽑아낸 다음 서비스 계층에 전달할 수 있어서 코드가 매우 간단해진다.
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
@GetMapping("/chat")
public ResponseEntity<ChatListResponseDto> getOneUserChat(@AuthUser Member member){
ChatListResponseDto oneUserChats = chatService.findOneUserChat(member.getId());
return new ResponseEntity<>(oneUserChats, HttpStatusCode.valueOf(200));
}
}
참고 문헌