Spring Security와 JWT를 사용하는 프로젝트에서 권한 정보를 다루는 과정에서 직면한 문제
JWT 토큰에서 유저의 권한 정보를 추출하고 처리하는 과정에서 발생한 문제를 살펴보겠다. 주로 유저의 권한 정보가 JWT
로부터 제대로 추출되지 않거나, UserRole
을 잘못 매핑하는 상황에서 발생하는 문제다.
JWT 토큰은 기본적으로 문자열 기반이기 때문에, 유저의 권한 정보를 추출할 때 String
형태로 추출된다. 하지만 프로젝트에서 UserRole
같은 Enum
타입을 사용하는 경우, 이를 적절히 변환하지 않으면 권한 정보가 일관되지 않을 수 있다.
예를 들어, JWT에서 userRole
을 추출한 후 이를 UserRole Enum
으로 변환하지 않고 문자열로만 처리하면 다음과 같은 문제를 마주할 수 있다:
문제 발생: 권한 정보를 Enum으로 변환하지 않아 권한을 확인할 때마다 매칭 오류가 발생한다.
해결 방법: JWT에서 권한 정보를 추출할 때, 이를 UserRole.of()
메서드를 사용해 Enum으로 변환하는 것이 필요하다.
String role = claims.get("userRole", String.class);
UserRole userRole = UserRole.of(role);
Spring Security는 기본적으로 GrantedAuthority
를 통해 유저의 권한을 관리한다. 하지만 프로젝트에서 UserRole
Enum을 사용하는 경우, GrantedAuthority
와의 변환 로직을 잘못 구현하면 권한 체크가 제대로 동작하지 않는다.
문제 발생: UserRole
과 GrantedAuthority
간의 매핑이 올바르게 설정되지 않아 권한이 일관되지 않다.
해결 방법: UserRole
Enum을 GrantedAuthority
로 변환하는 로직을 명확히 구현해야 한다.
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
또한, 권한을 확인할 때는 authorities
에서 권한 이름을 추출하고, 이를 UserRole
로 변환하는 로직을 작성해주었다
public UserRole getRole() {
return UserRole.of(authorities.iterator().next().getAuthority());
}
프로젝트를 진행 중 user_role
컬럼에 값을 저장할 때 다음과 같은 에러가 발생했다
SQL Error: 1265, SQLState: 01000
Data truncated for column 'user_role' at row 1
이 에러는 컬럼에 저장되는 값이 너무 커서 발생하는 문제이다. user_role
컬럼이 충분히 긴 문자열을 허용하지 않기 때문에 데이터가 잘려서 저장되거나 에러가 발생했다.
문제를 해결하기 위해 Query Console를 이용하여 직접 컬럼의 길이를 수정하기 위한 SQL 쿼리를 작성해주었다.
ALTER TABLE users MODIFY COLUMN user_role VARCHAR(20);
이 쿼리는 user_role
컬럼의 길이를 VARCHAR(20)
으로 변경하여 ROLE_USER
또는 ROLE_ADMIN
과 같은 값을 저장할 수 있도록 했다.
이 문제는 데이터베이스 컬럼의 길이가 충분하지 않아 발생한 에러로, VARCHAR(20)
으로 컬럼의 길이를 설정해주면서 문제를 해결 할 수 있었다.
Spring Boot 프로젝트에서 매니저 등록 요청 시 로그를 남기려고 했지만, 로그가 제대로 기록되지 않는 문제가 발생했다. 이 문제는 Propagation.REQUIRES_NEW
를 설정했음에도 불구하고, 로그가 기록되지 않는 것이 주요 이슈였다. 문제의 핵심은 로그 기록 메서드가 같은 서비스 클래스 내부에서 호출되고 있었기 때문이다. 이를 해결하기 위해 서비스 분리가 필요했다.
Spring에서 @Transactional
어노테이션은 같은 클래스 내부의 메서드를 호출할 때는 트랜잭션 전파가 적용되지 않는다는 특성이 있다. 매니저 등록과 로그 기록이 같은 서비스 클래스 내부에 있을 경우, Propagation.REQUIRES_NEW
설정이 적용되지 않아서 매니저 등록 트랜잭션과 로그 기록 트랜잭션이 제대로 분리되지 않았다.
즉, 매니저 등록에서 예외가 발생하면 로그 기록 트랜잭션도 함께 롤백되어 로그가 남지 않는 상황이었다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ManagerService {
private final ManagerRepository managerRepository;
private final UserRepository userRepository;
private final TodoRepository todoRepository;
private final LogRepository logRepository;
@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
logAction("매니저 등록 요청", "요청한 유저: " + authUser.getEmail() + ", todoId: " + todoId);
try {
// 매니저 등록 로직
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
.orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));
Manager newManager = new Manager(managerUser, todo);
Manager savedManager = managerRepository.save(newManager);
logAction("매니저 등록 성공", "매니저 ID: " + savedManager.getId());
return new ManagerSaveResponse(
savedManager.getId(),
new UserResponse(managerUser.getId(), managerUser.getEmail())
);
} catch (Exception e) {
logAction("매니저 등록 실패", "실패 이유: " + e.getMessage());
throw e; // 예외를 다시 던져 매니저 등록 실패로 처리
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action, String info) {
Log log = new Log(action, info);
logRepository.save(log);
}
}
초기에는 같은 클래스에서 해당 메서드를 만들어서 사용하였다.
문제 해결을 위해 로그 기록을 위한 메서드를 별도의 서비스로 분리하였다. 이를 통해 매니저 등록 트랜잭션과 로그 기록 트랜잭션이 독립적으로 작동하도록 만들었다. 분리된 로그 서비스는 Propagation.REQUIRES_NEW
설정을 통해 매니저 등록 트랜잭션과는 별개로 항상 로그가 기록되도록 했다.
LogService 생성
우선, 로그 기록을 위한 별도의 LogService
를 생성하였다. 이 서비스는 로그를 기록할 때마다 새로운 트랜잭션을 시작하도록 설정되었다.
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class LogService {
private final LogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action, String info) {
Log log = new Log(action, info);
logRepository.save(log);
}
}
ManagerService에서 로그 호출
이제 ManagerService
에서는 LogService
를 주입받아 매니저 등록 시 발생하는 로그를 기록하도록 변경하였다. 매니저 등록이 성공하든 실패하든, 로그는 항상 LogService
를 통해 독립적인 트랜잭션으로 기록된다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ManagerService {
private final ManagerRepository managerRepository;
private final UserRepository userRepository;
private final TodoRepository todoRepository;
private final LogService logService; // LogService 의존성 주입
@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
logService.logAction("매니저 등록 요청", "요청한 유저: " + authUser.getEmail() + ", todoId: " + todoId);
try {
// 매니저 등록 로직
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
.orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));
Manager newManager = new Manager(managerUser, todo);
Manager savedManager = managerRepository.save(newManager);
logService.logAction("매니저 등록 성공", "매니저 ID: " + savedManager.getId());
return new ManagerSaveResponse(
savedManager.getId(),
new UserResponse(managerUser.getId(), managerUser.getEmail())
);
} catch (Exception e) {
logService.logAction("매니저 등록 실패", "실패 이유: " + e.getMessage());
throw e; // 예외를 다시 던져 매니저 등록 실패로 처리
}
}
}
이번 이슈는 같은 서비스 내부에서 트랜잭션 경계가 분리되지 않아서 발생한 문제였다. Spring의 트랜잭션 전파 옵션은 제대로 설정되어도, 같은 클래스 내에서 호출할 경우 트랜잭션이 분리되지 않기 때문에 문제가 발생할 수 있다. 이런 경우에는 서비스 클래스를 분리하고, Propagation.REQUIRES_NEW
와 같은 옵션을 사용해 트랜잭션 경계를 명확히 하는 것이 중요하다.
참고! :처음에는 RDS에서 만든 db가 연결이 안되는 문제를 해결하기 위하여 포트 포워딩을 이용해 볼려고 했지만 중간에 문제가 해결되지 않아서 진행하지 못했다. 해당 문제를 모두 해결하지 못했지만, 문제를 해결하기 위해서 했던 방법들을 기록하고 해결하지 못한 문제를 해결하고 기록한 내용을 적용하고 싶어서 정리해보았다.
AWS EC2 인스턴스에 SSH 접속을 시도할 때 발생하는 "Permission denied (publickey)" 오류와 이를 해결하는 방법을 설명한 후, 포트 포워딩을 통해 RDS와 연결하는 방법을 시도했던 경험을 적어보았다.
EC2 인스턴스에 접근할 때 사용되는 프라이빗 키 파일은 AWS 콘솔에서 설정한 키 페어와 일치해야 한다. SSH 접속 시 퍼블릭 IP와 보안 그룹 설정도 올바르게 설정해야 한다.
1. Key Pair Name 확인: AWS 콘솔에서 EC2 인스턴스를 선택한 후 세부 정보에서 Key Pair Name이 SSH 접속 시 사용하는 키 파일과 일치하는지 확인한다.
2. 퍼블릭 IP 주소 확인:인스턴스가 할당된 퍼블릭 IP 주소가 정확한지 확인한다. 재부팅으로 IP가 변경되었는지도 확인해야 한다.
ssh -i ~/.ssh/newkeypair1.pem ec2-user@<퍼블릭 IP 주소>
프라이빗 키 파일의 권한이 너무 넓게 설정되면 SSH는 이를 거부한다. SSH가 소유자만 읽기 권한을 요구하기 때문이다.
sudo cp /mnt/c/Users/user/Downloads/newkeypair1.pem ~/.ssh/newkeypair1.pem
sudo chmod 400 ~/.ssh/newkeypair1.pem
ssh -i ~/.ssh/newkeypair1.pem ec2-user@<퍼블릭 IP 주소>
나는 파일 권한 설정 부분에서 권한이 너무 넓게 설정되었다는 오류가 지속 되었고 이를 해결하기 위해서 프라이빗 키 파일을 WSL의 홈 디렉토리로 복사하고, 권한을 다시 설정하는 방법도 사용해 보았지만 같은 문제가 반복되어서 발생하였다. 그래서 이 문제를 해결하고 다음으로 적는 해결 방법을 적용해 볼 생각이였다.
EC2 인스턴스의 포트 22(SSH)가 열려 있어야 SSH 접속이 가능하다. 보안 그룹이 올바르게 설정되었는지 확인한다.
기존 키 페어가 손상되거나 알맞지 않은 경우 일 수도 있기 때문이다
포트 포워딩은 EC2 인스턴스에서 RDS 데이터베이스에 접근할 수 있게 해준다. EC2 인스턴스와 RDS는 서로 다른 네트워크에 있을 수 있으므로, 포트 포워딩을 통해 데이터베이스에 안전하게 접근할 수 있다.
SSH를 통해 EC2 인스턴스에 접속하면서 포트 3306을 로컬 시스템과 EC2 인스턴스를 통해 RDS로 전달하는 포워딩을 설정한다.
ssh -i ~/.ssh/newkeypair1.pem -L 3306:<RDS-엔드포인트>:3306 ec2-user@<EC2-퍼블릭-IP>
-L 3306:<RDS-엔드포인트>:
3306: EC2를 통해 RDS의 MySQL 포트 3306에 연결한다.
<RDS-엔드포인트>:
AWS RDS 콘솔에서 확인할 수 있는 RDS 엔드포인트 주소를 입력한다.
<EC2-퍼블릭-IP>:
EC2 인스턴스의 퍼블릭 IP 주소를 입력한다.
이제 로컬에서 포트 3306을 통해 RDS 데이터베이스에 접근할 수 있다.
mysql -h 127.0.0.1 -P 3306 -u <DB-username> -p
이 명령어를 실행하면, 로컬에서 127.0.0.1을 통해 RDS에 접근할 수 있게 된다.