본문으로 바로가기
반응형

개요

모든 서버가 마찬가지이겠지만 개발을 하다보면 인증 관련된 로직을 단일화해서 사용해야할때가 오기 마련이다. 파이썬에서도 각 프레임워크에 맞게 작업을 해줬었는데 스프링에서는 크게 보면 Spring security와 Interceptor를 활용하여 작업할 수 있는 것으로 보인다. 이전 글(https://hides.tistory.com/1079)에서는 스프링 시큐리티 필터를 활용하여 인증 처리를 구현했는데 이번에는 인터셉터를 활용하여 인증 처리를 구현해봤다.

인터넷에서 그림을 찾아보다가 가장 이해하기 쉬운 그림인 것 같아서 가져왔다. 그림에서 나와있듯이 인터셉터의 경우 스프링 프레임워크단에서 제공해주는 기술이고 필터는 그렇지 않다. 따라서 인터셉터는 Servlet Container를 거친 이후에 실행되며 필터는 거치기 이전에 실행이 된다.

Interceptor 동작 순서

1. preHandler()
2. Controller 요청 처리
3. postHandler()
4. View 렌더링
5. afterCompletion()

Permission annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
    PermissionRole role() default PermissionRole.MEMBER;
}

먼저 권한 체크용도로 사용할 어노테이션을 하나 만들어준다. 위 어노테이션을 Controller쪽 파라미터로 줌으로써 엔드포인트마다 세분화된 권한 검사를 진행할 수 있도록 한다.

PermissionRole

public enum PermissionRole {
    ADMIN, MEMBER
}

본 예제에서는 관리자이거나 로그인된 유저인 경우 총 두가지의 권한만 검사할 예정이므로 위처럼 ADMIN, MEMBER 두가지의 enum값만 생성해줬다.

PermissionInterceptor

여기서부터 코드가 조금 길다. 천천히 하나하나 살펴보자.

public class PermissionInterceptor implements HandlerInterceptor

먼저 HandlerInterceptor 인터페이스를 구현한 클래스를 하나 만들어준다. 예전에는 HandlerInterceptorAdaptor를 상속받아서 구현하는 방법도 있었는데 스프링 5.3 버전부터 depreacted됐다고 한다.

preHandle()

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception

다음으로 preHandle() 메소드를 오버라이드한다. 위에서 설명한대로 해당 메소드는 Request를 Controller로 보내기전에 거치는 단계이다. preHandle() 메소드에 헤더로 들어온 토큰을 가져오고 특정한 작업을 수행하는 로직을 추가해야한다. request, response 파라미터는 이름 그대로 들어온 요청과 나갈 응답이라고 생각하면 되고 handler는 @Controller, @RequestMapping등의 어노테이션을 붙인 메소드이다. 

if (!(handler instanceof HandlerMethod)) {
    return true;
}

따라서 현재 입력으로 들어온 메소드가 @Controller, @RequestMapping 어노테이션이 붙었는지 확인한 후 그렇지 않다면 그냥 넘어가는 코드를 추가해줬다.

HandlerMethod handlerMethod = (HandlerMethod) handler;
Permission permission = handlerMethod.getMethodAnnotation(Permission.class);
if (permission == null) {
    return true;
}

위 코드는 핸들러에 위에서 생성한 Permission 어노테이션이 포함되어 있는지 검사하는 로직이다. 마찬가지로 없다면 그냥 넘어간다.

String token = extractJwtTokenFromHeader(request);
JwtTokenPayload payload = jwtTokenUtil.getPayload(token);
tokenPayloadContext.setJwtTokenPayload(payload);
userService.getUser(payload.getUserId());

if (permission.role().equals(PermissionRole.MEMBER)) {
    return true;
}

이쪽은 헤더에서 토큰을 디코딩하고 MEMBER에 해당하는 권한을 가지고있는지 검사하는 부분이다. 구현마다, 로직마다 다르므로 자세히 설명은 하지 않겠다. 서비스별로 검증할 로직을 넣어주면 된다. 또한 ADMIN에 대한 체크도 작성해주면 되는데 이 또한 생략한다.

throw new AuthenticationException();

마지막으로 모든 권한 검사에 실패했다면 예외를 발생시킨다.

@Component
@RequiredArgsConstructor
public class PermissionInterceptor implements HandlerInterceptor {
    private final JwtTokenUtil jwtTokenUtil;
    private final UserService userService;
    private final TokenPayloadContext tokenPayloadContext;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Permission permission = handlerMethod.getMethodAnnotation(Permission.class);
        if (permission == null) {
            return true;
        }

        String token = extractJwtTokenFromHeader(request);
        JwtTokenPayload payload = jwtTokenUtil.getPayload(token);
        tokenPayloadContext.setJwtTokenPayload(payload);
        userService.getUser(payload.getUserId());

        if (permission.role().equals(PermissionRole.MEMBER)) {
            return true;
        }

        // TODO: 관리자 권한관련 DB설계 후 수정 필요
        if (permission.role().equals(PermissionRole.ADMIN)) {
            return true;
        }

        throw new AuthenticationException();
    }

    private String extractJwtTokenFromHeader(HttpServletRequest request) {
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            throw new AuthenticationException();
        }

        try {
            return authorization.split(" ")[1];
        } catch (Exception e) {
            throw new AuthenticationException();
        }
    }
}

전체 코드는 위와 같다.

Config 설정

@Configuration
@RequiredArgsConstructor
public class AppConfig implements WebMvcConfigurer {
    private final PermissionInterceptor permissionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(permissionInterceptor);
    }
}

이제 설정부분에 우리가 생성한 인터셉터를 등록해줘야한다. WebMvcConfigurer를 구현한 Config 클래스를 하나 생성하고 addInterceptors() 메소드를 오버라이드하여 우리가 생성한 인터셉터를 등록해준다. 그러면 컨트롤러에 도달하기 전 등록한 인터셉터를 먼저 거치게된다.

컨트롤러에 적용

@Permission(role = PermissionRole.MEMBER)
@GetMapping("/me")
public ResponseEntity<UserProfileDto> getProfileInfo(@CurrentUser UserInfo user) {
    UserProfileDto userProfile = userProfileQueryService.getUserProfile(user.getId());
    return ResponseEntity.ok(userProfile);
}

적용은 간단하다. 위처럼 Permission 어노테이션을 지정해주고, role부분에 관리자에 대한 검사를 진행할 지 단순 로그인된 유저에 대해 검증할 지 표시해주면 된다.

반응형