Series/내가 해본

Spring Security 로 회원 가입을 구현해보자 (3)

Hyunec 2021. 6. 19. 20:10

Step 4. Custom Annotation 을 통해 로그인 정보 가져오기

1. Custom Annotation 구현

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface LoginMember {

  boolean required() default true;
}

annotation 은 기본 값을 true 로 두고 필요한 api 에서만 사용합니다.

@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

  private static final String AUTHORIZATION_HEADER = "authorization";

  private final JwtAuthTokenProvider jwtAuthTokenProvider;
  private final MemberRepository memberRepository;

  @Override
  public boolean supportsParameter(MethodParameter methodParameter) {
    return methodParameter.hasParameterAnnotation(LoginMember.class);
  }

  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
    LoginMember loginUserAnnotation = parameter.getParameterAnnotation(LoginMember.class);
    if (!loginUserAnnotation.required()) {
      return null;
    }

    HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

    return Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER))
        .map(authorization -> authorization.split("bearer")[1])
        .map(jwtAuthTokenProvider::convertAuthToken)
        .map(JwtAuthToken::getEmail)
        .map(email -> memberRepository.findByEmail(email)
            .orElseThrow(() -> new CustomAuthrizationException(ErrorCode.NOT_EXIST_MEMBER)))
        .orElseThrow(() -> new CustomAuthrizationException(ErrorCode.FORBIDDEN));
  }
}

resolver 는 jwt 를 해석해서 이메일 정보를 가져오고, 회원이 존재하는지를 검증합니다.

2. Resolver 설정

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  private final LoginMemberArgumentResolver loginMemberArgumentResolver;

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(loginMemberArgumentResolver);
  }
}

3. JWT 에서 로그인 정보 가져오기

@RestController
public class LoginController {

  @GetMapping("/api/v1/my")
  public void my(@LoginMember Member loginMember) {
    log.info("### loginMember.getEmail()={}, loginMember.getRole()={}", loginMember.getEmail(), loginMember.getRole());
  }
}

controller 에서 jwt 에 없던 id 정보를 확인할 수 있습니다.

마치며

구현을 하면서 배운 것도 많았지만, 반대로 미숙한 부분도 많았습니다.

  • filter 레이어에서의 에러 처리
    • 인증/인가 예외 처리를 제대로 하고 있는가?
  • AuthTokenProvider 에 Authentication 을 가져오는 메소드가 있는게 적절한가?
  • UsernamePasswordAuthenticationToken 외를 이용해서 인증을 하려면 어떻게 해야할까?
  • SecurityContextHolder 를 더 잘 이용할 수 있는 방법은 무엇일까?

특히 예제에서는 회원 정보를 가져올 때 SecurityContextHolder 를 통한 것이 아닌 별도의 Resolver 를 만들었습니다.
조금 구현해본 바로는 Authentication - User - principal 와 Adapter 를 이해해야되는데,
이 과정에서 Member entity 에 extends 를 해줘야하는 등 종속성이 생기는 것 같아 이상한 느낌이 들었습니다.

아마 공부가 부족해서 잘못 이해했거나 best practice 를 찾지 못한 것이라고 생각합니다.
하지만 security 는 워낙 양이 방대하고 사이드 프로젝트를 하는데는 이 정도의 구현으로 충분하기에
고도화는 좀 더 공부하고 해볼 생각입니다.

금방 할 수 있기를..