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();
}
}
주요 설명:
- 세션 확인: 현재 요청의 세션을 확인하고, 세션에 저장된 사용자의 로그인 상태를 체크합니다.
- ID 확인: 세션에서 로그인된 유저 ID 또는 관리자 ID를 가져옵니다.
- DB 연동:
UserProfileMapper
를 통해 데이터베이스에서 사용자의 정보를 가져옵니다. - 권한 확인:
LoginCheck
어노테이션에 지정된 역할과 사용자 권한이 일치하는지 확인합니다. - 메서드 실행: 인증 및 인가를 통과하면 원래의 메서드를 실행합니다.
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 |