회사 프로젝트 진행 중 권한 체크를 효율적으로 할 수 있을지에 대해 고민한 과정을 정리해보았다.
회사 요구사항을 보니 Role이 5개가 있었다.
표로 정리해서 보여주자면
권한 | A_Role | B_Role | C_Role | D_Role | ... |
---|---|---|---|---|---|
a권한 | o | x | x | x | |
b권한 | o | o | x | x | |
c권한 | o | x | o | x | |
d권한 | o | o | o | o | |
... | ... | ... | ... | ... | ... |
위와 같은 식으로 각각 다른 권한범위를 가지고 있다.
요구사항에 따라 필요한 인증 및 권한 직업은
다음과 같은 종류의 작업이 필요했다.(차후에 더 변경될 수 있었음)
지금까지의 권한 체크는 각각의 서비스 계층에서 발생하고 있었다.
AService.java
public void 권한 체크가 필요한 메서드(..., String email) {
MemberRole memberRole = memberFindService.findByEmail(email).getRole();
validateMemberRole(memberRole);
...
}
이러한 방식은 중복되는 코드를 만들어내고 하나의 메서드가 여러 역할을 가지게 하여 단일 책임 원칙(SRP)에도 위반되고 있었다.
Interceptor는 Dispatcher Servlet이 컨트롤러를 호출하기 전/후에 적용된다.
그 덕분에 스프링 컨텍스트 내부에서 Controller에 관한 요청, 응답을 처리할 수 있다.
Interceptor를 활용하면 중복되는 코드를 없에고 혹시 모를 코드 누락 가능성을 없엘 수 있어서 Interceptor를 활용하기로 하였다.
(AOP도 후보군에 고려를 하였으나 컨트롤러는 각각 사용하는 파라미터나 리턴 값이 다 달라서 AOP 보단 Interceptor를 사용하는게 낫다고 판단이 들었다. AOP를 사용한 로깅 시스템은 다른 포스팅에서 준비해보겠다.)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckRole {
Role[] role() default {Role.MEMBER};
enum Role {
DIVISION_MANAGER("Division Manager", 1)
, DIVISION_OBSERVER("Division Observer",2)
, TEAM_MANAGER("Team Manager",3)
, TEAM_OBSERVER("Team Observer",4)
, MEMBER("member",5);
@Getter
private String role;
@Getter
private Integer number;
Role(String role, Integer number) {
this.role = role;
this.number = number;
}
public static int checkUserRoleNumber(String role) {
return Arrays.stream(Role.values())
.filter(r -> r.role.equals(role))
.findFirst()
.orElseThrow(() -> new BusinessLogicException(BusinessErrorCode.MEMBER_MEMBER_ROLE_NOT_FOUND))
.getNumber();
}
}
}
일단 컨트롤러 메서드에 부착할 어노테이션을 만들었다.
API에 접근할 수 있는 역할이 여러 개일 수 있으므로 ROLE[] 배열로 지정하였고
아래에는 enum Role
과 Role을 찾아낼 수 있는 로직을 만들어냈다.
@Slf4j
@RequiredArgsConstructor
@Component
public class CheckRoleInterceptor implements HandlerInterceptor {
private final MemberMemberRoleFindService memberMemberRoleFindService;
//동작 이전에 권한 체크를 위해 preHandle 사용
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("# interceptor start");
if(!(handler instanceof HandlerMethod)) {
return true;
}
// Controller에 있는 메서드인지 확인하기 위해 HandlerMethod 타입인지 체크
HandlerMethod handlerMethod = (HandlerMethod) handler;
CheckRole checkRole = handlerMethod.getMethodAnnotation(CheckRole.class);
if(checkRole == null) return true;
//@checkRole 이 부착되지 않았다면 true 리턴
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!authentication.isAuthenticated()) return false;
// 로그인 하지 않았다면 false 리턴
String email = authentication.getName();
log.info("# email={}", email);
MemberMemberRoleDto.CheckMemberRoleDto checkMemberRoleDto = memberMemberRoleFindService.selectMemberRoleByEmail(email);
log.info("# checkMemberRoleDto = {}", checkMemberRoleDto);
if(Objects.equals(checkMemberRoleDto.getRole(), "SM")) return true;
//시스템 매니저(Admin)이라면 무조건 통과
String userRole = checkMemberRoleDto.getMemberRole();
int roleNumber = CheckRole.Role.checkUserRoleNumber(userRole);
log.info("# userRole = {}, roleNumber={}", userRole, roleNumber);
boolean checkUserRole = false;
for (CheckRole.Role role : checkRole.role()) {
if(role.compareTo(DIVISION_MANAGER) == 0) {
if(userRole.equals(DIVISION_MANAGER.getRole())) checkUserRole = true;
break;
}
if(role.compareTo(DIVISION_OBSERVER) == 0) {
if(userRole.equals(DIVISION_OBSERVER.getRole())) checkUserRole = true;
break;
}
if(role.compareTo(TEAM_MANAGER) == 0) {
if(userRole.equals(TEAM_MANAGER.getRole())) checkUserRole = true;
break;
}
if(role.compareTo(TEAM_OBSERVER) == 0) {
if(userRole.equals(TEAM_OBSERVER.getRole())) checkUserRole = true;
break;
}
if(role.compareTo(MEMBER) == 0) {
if(userRole.equals(MEMBER.getRole())) checkUserRole = true;
break;
}
}
return checkUserRole;
//어노테이션에 있는 ROLE 중 User가 하나라도 해당이 된다면 통과
}
}
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final CheckRoleInterceptor checkRoleInterceptor;
@Override //어노테이션 적용
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkRoleInterceptor);
}
}