대규모 트래픽 게시판 구축 시리즈 #7: Spring AOP를 활용한 인증 및 인가

2024. 9. 5. 15:32·프레임워크/자바 스프링

Spring AOP와 MyBatis를 활용한 인증 및 인가 기능 구현: LoginCheck와 LoginCheckAspect

이번 글에서는 Spring AOP와 MyBatis를 활용하여 인증(Authentication)과 인가(Authorization) 기능을 구현하는 방법을 설명하겠습니다. 특히, LoginCheck라는 커스텀 어노테이션을 사용하여 특정 API에 접근할 때 사용자의 로그인 상태와 권한(Role)을 검사하는 과정을 다룹니다.

1. AOP(Aspect-Oriented Programming)란?

AOP는 핵심 비즈니스 로직과 관련 없는 공통된 로직(예: 인증, 인가, 로깅 등)을 횡단 관심사로 분리하여 처리하는 프로그래밍 기법입니다. AOP를 사용하면 코드의 모듈화가 용이해지며, 중복 코드 작성이 줄어들고 유지보수성이 향상됩니다.

2. 인증 및 인가 개요

  • 인증(Authentication): 사용자가 누구인지 확인하는 과정입니다. 보통 로그인 과정을 통해 사용자가 인증됩니다.
  • 인가(Authorization): 사용자가 특정 작업을 수행할 권한이 있는지 확인하는 과정입니다. 예를 들어, 일반 사용자와 관리자가 접근할 수 있는 자원이 다를 수 있습니다.

이번 구현에서는 LoginCheck 어노테이션과 이를 처리하는 AOP 클래스인 LoginCheckAspect를 통해 특정 API 호출 시 인증과 인가를 검사할 것입니다.

3. LoginCheck 어노테이션

LoginCheck는 메서드에 적용되는 커스텀 어노테이션으로, 특정 역할(Role)을 가진 사용자만 접근할 수 있도록 제한할 수 있습니다. 이 어노테이션은 AOP를 통해 적용된 메서드의 실행 전후에 추가적인 인증 및 권한 검사를 처리합니다.

LoginCheck 코드:

package com.example.boardserver.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginCheck {
    String[] roles() default {}; // 기본적으로 모든 사용자가 접근 가능, 특정 역할 지정 가능
}

주요 설명:

  • @Target(ElementType.METHOD): 이 어노테이션은 메서드에 적용됩니다.
  • @Retention(RetentionPolicy.RUNTIME): 런타임 시점에도 어노테이션 정보가 유지됩니다.
  • roles(): 해당 메서드에 접근할 수 있는 사용자의 권한을 지정합니다. 예를 들어, roles = {"ADMIN"}을 지정하면 관리자만 접근할 수 있습니다.

4. LoginCheckAspect: 인증 및 인가 처리

LoginCheckAspect는 AOP를 활용해 LoginCheck 어노테이션이 적용된 메서드가 호출될 때, 로그인 여부와 역할(Role)을 검사하는 역할을 합니다.

LoginCheckAspect 코드:

package com.example.boardserver.aop;

import com.example.boardserver.dto.UserDTO;
import com.example.boardserver.mapper.UserProfileMapper;
import com.example.boardserver.utils.SessionUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;

import java.util.Arrays;

@Component
@Aspect
@Log4j2
public class LoginCheckAspect {

    @Autowired
    private HttpServletRequest request; // HTTP 요청 객체를 주입받음

    @Autowired
    private UserProfileMapper userProfileMapper; // MyBatis Mapper를 통해 DB와 상호작용

    @Around("@annotation(com.example.boardserver.aop.LoginCheck) && @annotation(loginCheck)")
    public Object authenticateUser(ProceedingJoinPoint proceedingJoinPoint, LoginCheck loginCheck) throws Throwable {
        HttpSession session = request.getSession(false); // 현재 세션을 가져옴 (세션이 없으면 null 반환)

        // 1. 세션 또는 사용자 정보가 없는 경우 인증 실패 처리
        if (session == null || (session.getAttribute(SessionUtil.LOGIN_MEMBER_ID) == null
                && session.getAttribute(SessionUtil.LOGIN_ADMIN_ID) == null)) {
            log.error("인증되지 않은 사용자: " + proceedingJoinPoint.toString());
            throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "User is not authenticated") {};
        }

        // 2. 세션에서 사용자 또는 관리자 ID를 확인
        String id = SessionUtil.getLoginMemberId(session);
        if (id == null) {
            id = SessionUtil.getLoginAdminId(session);
        }

        if (id == null) {
            log.error("세션에 사용자 ID가 없음: " + proceedingJoinPoint.toString());
            throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "User ID not found in session") {};
        }

        // 3. DB에서 사용자의 정보를 가져옴
        UserDTO user = userProfileMapper.getUserProfile(id);
        if (user == null) {
            log.error("해당 ID로 사용자를 찾을 수 없음: " + id);
            throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "User not found") {};
        }

        // 4. 사용자 권한 확인
        if (loginCheck.roles().length > 0 && !Arrays.asList(loginCheck.roles()).contains(user.getStatus().toString())) {
            log.error("필요한 역할을 가지고 있지 않음: " + user.getStatus());
            throw new HttpStatusCodeException(HttpStatus.FORBIDDEN, "User does not have required role") {};
        }

        // 5. 인증 및 인가 통과 후 메서드 실행
        return proceedingJoinPoint.proceed();
    }
}

주요 설명:

  1. 세션 확인: 현재 요청의 세션을 확인하고, 세션에 저장된 사용자의 로그인 상태를 체크합니다.
  2. ID 확인: 세션에서 로그인된 유저 ID 또는 관리자 ID를 가져옵니다.
  3. DB 연동: UserProfileMapper를 통해 데이터베이스에서 사용자의 정보를 가져옵니다.
  4. 권한 확인: LoginCheck 어노테이션에 지정된 역할과 사용자 권한이 일치하는지 확인합니다.
  5. 메서드 실행: 인증 및 인가를 통과하면 원래의 메서드를 실행합니다.

5. SessionUtil 클래스: 세션 관리

SessionUtil은 세션에서 로그인된 사용자 정보(일반 사용자 또는 관리자)를 가져오거나, 세션을 초기화하는 유틸리티 클래스입니다.

SessionUtil 코드:

package com.example.boardserver.utils;

import jakarta.servlet.http.HttpSession;

public class SessionUtil {
    public static final String LOGIN_MEMBER_ID = "LOGIN_MEMBER_ID";
    public static final String LOGIN_ADMIN_ID = "LOGIN_ADMIN_ID";

    private SessionUtil(){}

    public static String getLoginMemberId(HttpSession session) {
        return (String) session.getAttribute(LOGIN_MEMBER_ID);
    }

    public static void setLoginMemberId(HttpSession session, String id){
        session.setAttribute(LOGIN_MEMBER_ID, id);
    }

    public static String getLoginAdminId(HttpSession session) {
        return (String) session.getAttribute(LOGIN_ADMIN_ID);
    }

    public static void setLoginAdminId(HttpSession session, String id){
        session.setAttribute(LOGIN_ADMIN_ID, id);
    }

    public static void clear(HttpSession session) {
        session.invalidate(); // 세션 무효화
    }
}

주요 설명:

  • getLoginMemberId: 세션에서 로그인된 일반 사용자의 ID를 가져옵니다.
  • getLoginAdminId: 세션에서 로그인된 관리자의 ID를 가져옵니다.
  • setLoginMemberId: 로그인 시 세션에 사용자 ID를 저장합니다.
  • setLoginAdminId: 관리자가 로그인했을 경우, 세션에 관리자 ID를 저장합니다.
  • clear: 로그아웃 시 세션을 무효화하여 모든 세션 데이터를 삭제합니다.

6. 실제 적용 예시

다음은 LoginCheck 어노테이션을 사용하여 인증 및 인가 처리를 구현한 예시입니다.

@GetMapping("/my-info")
@LoginCheck(roles = {"DEFAULT", "ADMIN"}) // 일반 사용자와 관리자 모두 접근 가능
public ResponseEntity<CommonResponse<UserInfoResponse>> memberInfo(HttpSession session) {
    UserDTO memberInfo = userService.getUserInfo(SessionUtil.getLoginMemberId(session));
    if (memberInfo == null) {
        return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
    }
    return new ResponseEntity<>(new CommonResponse<>("SUCCESS", memberInfo), HttpStatus.OK);
}
  • @LoginCheck(roles = {"DEFAULT", "ADMIN"}): 이 어노테이션을 통해 해당 엔드포인트는 로그인된 사용자와 관리자 모두 접근할 수 있습니다.
  • 세션을 통한 사용자 정보 가져오기: SessionUtil을 통해 세션에서 사용자의 ID를 가져오고, 이를 바탕으로 유저 정보를 조회합니다.

7. 정리 및 결론

이번 글에서는 Spring AOP와 MyBatis를 사용하여 인증(Authentication) 및 인가(Authorization) 기능을 구현하는 방법을 설명했습니다. 핵심적인 내용은 다음과 같습니다:

  • AOP를 통한 인증 및 인가 처리: AOP는 비즈니스 로직과 횡단 관심사를 분리하여 코드의 재사용성과 유지보수성을 높여줍니다. LoginCheckAspect 클래스는 AOP를 사용해 로그인 상태 및 사용자 권한을 검사하는 역할을 수행합니다.
  • LoginCheck 어노테이션: 메서드에 커스텀 어노테이션을 적용하여 특정 권한을 가진 사용자만 해당 메서드에 접근할 수 있도록 제한할 수 있습니다. 이를 통해 코드의 중복을 줄이고, 각 엔드포인트의 접근 제어를 쉽게 관리할 수 있습니다.
  • 세션 관리: SessionUtil 클래스를 통해 로그인 상태를 유지하고 세션을 관리하며, 사용자 정보와 역할을 세션에 저장하고 불러올 수 있습니다.
  • MyBatis와의 연동: UserProfileMapper를 사용해 데이터베이스에서 사용자의 정보를 가져오고, 해당 사용자의 권한을 기반으로 메서드 실행 여부를 결정합니다.

이 방식은 유저 인증 및 권한 검사가 필요한 엔드포인트에서 매우 유용하며, AOP를 통해 각 메서드의 로직과 인증/인가 로직을 분리하여 유지보수를 용이하게 할 수 있습니다. 특히 대규모 트래픽을 처리하는 시스템에서 유연한 권한 관리를 제공하며, 필요에 따라 추가적인 검증 로직을 쉽게 확장할 수 있습니다.

다음 글에서는 트랜잭션 처리 및 보안 강화와 관련된 추가적인 기능에 대해 다뤄보겠습니다.

저작자표시 (새창열림)

'프레임워크 > 자바 스프링' 카테고리의 다른 글

대규모 트래픽 게시판 구축 시리즈 #9: 게시판 API  (0) 2024.09.07
대규모 트래픽 게시판 구축 시리즈 #8: 카테고리 API  (0) 2024.09.07
대규모 트래픽 게시판 구축 시리즈 #6: 유저 API  (1) 2024.09.05
대규모 트래픽 게시판 구축 시리즈 #5: MySQL 데이터베이스 연결 설정  (1) 2024.09.05
대규모 트래픽 게시판 구축 시리즈 #4: 프로젝트 초기 셋업  (3) 2024.09.05
'프레임워크/자바 스프링' 카테고리의 다른 글
  • 대규모 트래픽 게시판 구축 시리즈 #9: 게시판 API
  • 대규모 트래픽 게시판 구축 시리즈 #8: 카테고리 API
  • 대규모 트래픽 게시판 구축 시리즈 #6: 유저 API
  • 대규모 트래픽 게시판 구축 시리즈 #5: MySQL 데이터베이스 연결 설정
hyeseong-dev
hyeseong-dev
안녕하세요. 백엔드 개발자 이혜성입니다.
  • hyeseong-dev
    어제 오늘 그리고 내일
    hyeseong-dev
  • 전체
    오늘
    어제
    • 분류 전체보기 (282)
      • 여러가지 (107)
        • 알고리즘 & 자료구조 (72)
        • 오류 (4)
        • 이것저것 (29)
        • 일기 (1)
      • 프레임워크 (39)
        • 자바 스프링 (39)
        • React Native (0)
      • 프로그래밍 언어 (38)
        • 파이썬 (30)
        • 자바 (3)
        • 스프링부트 (5)
      • 운영체제 (0)
      • DB (17)
        • SQL (0)
        • Redis (17)
      • 클라우드 컴퓨팅 (2)
        • 도커 (2)
        • AWS (0)
      • 스케쥴 (65)
        • 세미나 (0)
        • 수료 (0)
        • 스터디 (24)
        • 시험 (41)
      • 트러블슈팅 (1)
      • 자격증 (0)
        • 정보처리기사 (0)
      • 재태크 (4)
        • 암호화폐 (4)
        • 기타 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자바
    AWS
    Redis
    취업리부트
    프로그래머스
    ecs
    mybatis
    OOP
    java
    파이썬
    EC2
    SAA
    항해99
    celery
    DP
    시험
    Spring WebFlux
    WebFlux
    spring
    docker
    Docker-compose
    그리디
    FastAPI
    Spring Boot
    RDS
    reactor
    완전탐색
    #개발자포트폴리오 #개발자이력서 #개발자취업 #개발자취준 #코딩테스트 #항해99 #취리코 #취업리부트코스
    백준
    Python
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
hyeseong-dev
대규모 트래픽 게시판 구축 시리즈 #7: Spring AOP를 활용한 인증 및 인가
상단으로

티스토리툴바