들어가기 전에
사이드 프로젝트로 진행했던 프로젝트에서 사용자 권한이 USER, COMPANY, INSTRUCTOR, ADMIN으로 나누어져 있었고 각 권한마다 접근이 가능한 API가 달라 권한 체크 로직이 필요한 상황이었다.
프로젝트 아키텍처는 MSA 구조였으며, Spring Cloud Gateway에서 JWT 토큰으로부터 사용자 PK와 권한 정보를 추출하여 각 마이크로서비스로 라우팅하고 있었다. 따라서, 해당 권한 정보를 기반으로 API 접근을 제한할 수 있을 것이라고 판단했다.
Spring MVC 레벨에서 동작하며 컨트롤러 진입 전후에 특정 로직을 처리하는 Interceptor와 비즈니스 로직과 서브로직을 분리하여 횡단 관심사를 처리하는 AOP 중 고민했지만 특정 URI 경로에 대해 권한을 검사하는 Interceptor와 달리 커스텀 어노테이션으로 쉽게 권한을 체크할 API를 지정할 수 있는 AOP를 사용하기로 했다.
구현하기
RoleAuthorization
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoleAuthorization {
String[] roles();
}
AOP 포인트 컷으로 사용할 커스텀 어노테이션이다. 해당 어노테이션이 붙은 메소드에서는 권한을 체크해야 한다.
- @Retention : 런타임 시까지 해당 어노테이션이 유지된다는 것을 의미, 어플리케이션이 실행 중에도 이 어노테이션 정보를 참조할 수 있다.
- @Target : 해당 어노테이션이 메소드에만 적용될 수 있음을 의미한다.
- String[] roles(): 어노테이션을 선언할 때 접근가능한 권한을 선언하기 위한 변수
RoleAuthorizationAop
@Aspect
@Component
public class RoleAuthorizationAop {
@Pointcut("@annotation(com.example.companyservice.common.security.RoleAuthorization)")
public void roleAuthorizationPointcut() {}
@Around("roleAuthorizationPointcut()")
public Object roleAuthorizationAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
RoleAuthorization roleAuthorization = signature.getMethod().getAnnotation(RoleAuthorization.class);
String[] requiredRoles = roleAuthorization.roles();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes()).getRequest();
String userRole = Utils.parseRole(request);
if (Arrays.asList(requiredRoles).contains(userRole)) {
return joinPoint.proceed();
} else {
throw new ApiException(ExceptionEnum.ACCESS_NOW_ALLOW_EXCEPTION);
}
}
}
RoleAuthorization 어노테이션이 선언되어 있는 메소드가 호출됐을 때 실행되는 권한체크 로직이다.
어노테이션을 포인트 컷 지시자로 지정하기 위해 roleAuthorizationPointcut 메소드를 생성하고 @Pointcut 어노테이션에 RoleAuthorization 어노테이션 인터페이스의 패키지 경로를 작성해주었다.
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
RoleAuthorization roleAuthorization = signature.getMethod().getAnnotation(RoleAuthorization.class);
String[] requiredRoles = roleAuthorization.roles();
ProceedingJoinPoint의 Signature로부터 어노테이션 정보를 추출하고 어노테이션에 선언된 권한 정보를 조회한다. 그런 다음 Spring Cloud Gateway로부터 전달받은 권한과 비교하여 포함하면 정상적으로 메소드를 호출하고 포함하지 않으면 권한 에러를 발생한다.
어노테이션 포인트 컷 지시자는 아래와 같이 어노테이션 인터페이스를 인자로 받음으로써 더 간단하게 지정할 수 있다.
@Aspect
@Component
public class RoleAuthorizationAop {
@Around("@annotation(roleAuthorization)")
public Object roleAuthorizationAdvice(ProceedingJoinPoint joinPoint, RoleAuthorization roleAuthorization) throws Throwable {
String[] requiredRoles = roleAuthorization.roles();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes()).getRequest();
String userRole = Utils.parseRole(request);
if (Arrays.asList(requiredRoles).contains(userRole)) {
return joinPoint.proceed();
} else {
throw new ApiException(ExceptionEnum.ACCESS_NOW_ALLOW_EXCEPTION);
}
}
}
이제 권한 체크를 위한 AOP 설정이 모두 끝났다.
특정 API에 대해 접근을 제한하고 싶으면 아래와 같이 메소드에 @RoleAuthorization 어노테이션을 선언하고 허용 권한을 명시해주면 된다.
@GetMapping("/members")
@RoleAuthorization(roles = {"USER"})
@Operation(summary = "마이페이지 - 내 정보 수정 화면에 필요한 정보 조회", description = "내 정보 수정을 위한 내 정보 조회\n[피그마 링크](https://www.figma.com/file/nYEBH6aqCI37ZX0X6w7Ena?embed_host=notion&kind=file&mode=design&node-id=9914-16871&t=T0LzXHd8One1Acu9-0&type=design&viewer=1)")
@ApiResponse(responseCode = "200", description = "성공", content = @Content(schema = @Schema(implementation = MemberDetailResponseDto.class)))
public ResponseEntity<BaseResponseDto<MemberDetailResponseDto>> getMemberDetail(HttpServletRequest request) {
long memberId = Utils.parseAuthorizedId(request);
return ResponseEntity.status(HttpStatus.OK)
.body(new BaseResponseDto<>(memberService.getMemberDetail(memberId)));
}
이번 프로젝트에서 AOP와 커스텀 어노테이션을 활용해 권한 체크 로직을 구현함으로써 코드의 중복을 줄이고 보다 모듈화된 방식으로 권한 관리를 처리할 수 있었습니다.
'Spring' 카테고리의 다른 글
[Spring] 상속 구조와 Static Inner Class로 Dto 클래스 관리하기 (0) | 2024.09.21 |
---|---|
[Spring] 추상 팩토리 패턴을 사용하여 소셜 로그인 구현하기 (1) | 2024.09.16 |
헥사고날 아키텍처(Hexagonal Architecture) (0) | 2024.01.19 |
[Spring] Master, Slave 데이터베이스 구조로 쓰기, 읽기 연산 나누기 (0) | 2023.07.28 |
[Spring] 디자인 패턴 - 전략 패턴(Strategy Pattern) (0) | 2023.07.01 |