여러가지 이야기

[Spring/Spring Security] 유저 CRUD 기능 권한 검증하기 by 메서드 보안 - 트러블 슈팅 본문

study/Spring

[Spring/Spring Security] 유저 CRUD 기능 권한 검증하기 by 메서드 보안 - 트러블 슈팅

jimddong 2024. 7. 12. 00:03

동아리 프로젝트에서 회원가입, 로그인 기능을 구현하는 걸 맡았었다. Spring Security를 공부하면서, 인가 인증을 구현하며 다양한 문제를 마주했었다.

 

이번 글은 '멤버와 관련된 CRUD 기능을 권한에 따라 가능하게 혹은 불가능하게 만들고 싶은데 코드를 어떻게 짜야하지?'에서 시작되었다. 

 

 

🚨 이슈

멤버가 자기 자신만의 정보를 수정(Update)하거나 자신이 가입한 계정을 탈퇴(Delete)하려 한다.

당연하게도 한 회원이 타 회원의 정보를 수정할 수 있으면 안된다. 또한 당연한 말이지만... 한 회원이 다른 회원 계정을 삭제 시켜버리는 것 역시 안된다. 하지만 내가 처음 짠 코드는 이 두 가지가 모두 가능했었다. (허허)

 

이와 같은 불상사를 막으려면 즉, 멤버가 본인의 정보만을 수정 or 본인의 계정만을 탈퇴할 수 있게끔 하려면 코드를 어찌 수정해야할지 고민했다.

 


 

📋 문제

MemberController
MemberServiceImpl

MemberController단에서 updateMember 혹은 deleteMember api에 접근하려 할 때, MemberService의 메서드들로 이어지고, 서비스 인터페이스의 구현체인 MemberServiceImpl에서는 MemberController로부터 전달 받은 memberId 정보를 확인하여 메서드를 실행시킨다.

 

여기서 멤버가 본인의 정보만을 수정 or 본인의 계정만을 탈퇴할 수 있게 해야한다면, 전달 받은 memberId는 본인의 memberId여야만 할 것이다. 이걸 검증할 무언가를 코드에 추가해야한다.

 


✏️ 해결 과정

접근 방법 1) SecurityConfig 필터 체인 내에서 부여하는 권한과 역할을 활용해보기(?)

내 SecurityConfig 파일 중 일부
유저 정보를 담는 역할을 하는 인터페이스, UserDetails를 새 클래스로 구현하였다. 회원 가입 시 멤버 정보를 입력할 때 ROLE_USER라고 역할이 자동으로 부여되게끔 했다. (roles.add)

더보기

* Spring Security 내에 있는 인터페이스인 UserDetails UserDetailsService는 각각 유저의 정보를 담는 인터페이스, 유저의 정보를 가져오는 인터페이스이다. 

앞서 SecurityConfig 파일에 기본 필터체인을 Bean으로 등록해두었는데, 그 안에 허용 url을 설정했었다. 특정 역할을 가진(hasRole) 어떠한 요청들(requests)에 대하여 권한을 허용하는 내용이다. '어찌 됐든 권한과 관련이 있으니 얘를 건들면 되지 않을까?'하는 1차원적인 생각으로 접근해보려 했다.

애초에 모든 멤버들이 ROLE_USER라는 역할을 부여받았다는 점에서, 지금 수정하려는 게 내 정보인지 다른 멤버의 정보인지, 역할로는 당연하게도 구분할 수가 없다.

 

하지만 Spring Security만으로 위처럼 URI 패턴에 따라 권한을 부여할 수 있지만, Spring Security 필터체인에서 특정 리소스에 대한 소유권을 확인하는 등(특정 멤버가 수정을 위해 접근하려 하는 것이 나의 정보가 맞는지...)의 기능은 없다. 즉, 이런 처리는 권한 부여가 아니라 권한 검증 측면에서 보아야한다!

 

 

접근 방법 2) Spring Security 메서드 보안 - 권한 검증 메서드로 만들기

특정 멤버가 정보 수정(update)을 시도하는데, 정녕 나의 정보를 수정하려는 게 맞는지 확인하기 위해서는 권한을 검증해야한다.

 

Spring Security에서 유저 정보를 담는 인터페이스에 해당하는 UserDetails를 구현하였는데, 각 유저 정보는 UserDetails를 구현한 클래스(내 코드에서는 PrincipalDetails) 인스턴스인 Pricipal이라는 객체에 담긴다.

 

이 점을 활용해 서비스 단에서 권한 검증 메서드를 만들 수 있다. MemberServiceImpl에서, MemberController로부터 전달 받은 memberId 값과 현재 JWT로 로그인한 Principal 객체의 값이 일치하는지 확인하는 것이다. (대신, memberId 값을 Principal 객체에서 가져와야하므로 UserDetails 구현 클래스에 getMemberId() 메서드를 추가하는 걸 잊지 말자)

 

방법은 아래와 같다.

1️⃣ @PreAuthorize라는 어노테이션을 서비스 단의 내가 원하는 메서드 위에 사용하고, 메서드 보안이 활성화 될 수 있게 2️⃣ @EnableGlobalMethodSecurity(prePostEnabled = true) 어노테이션을 SecurityConfig 클래스 위에 붙여준다.

#memberId == principal.memberId 표현식으로, 메서드 호출 전에 현재 인증된 사용자의 memberId가 요청 memberId와 동일한지 확인한다. 이를 통해 메서드를 호출할지 말지 결정한다. 현재 인증된 사용자의 memberId가 요청 memberId와 다르다면, 즉 다른 사람의 정보를 수정하려한다면 메서드가 호출되지 않고 에러 메시지가 뜨게끔 한다.

이처럼 메서드 보안을 활성화 시키면 세부적으로 권한을 제어하고, 표현식 기반으로 권한을 검증할 수 있다.

 


코드를 짜면서 이렇게도 저렇게도 접근하다보면 '이건 뭐고, 저건 뭐지?'하면서 자연스럽게 여러 개념을 접하면서 공부하게 된다. 내가 어디서 구멍이 있는지도 속속들이 드러난다😇 (더 공부해야한다는 걸 뼈저리게 느끼며,,,)

 

이번 글처럼 앞으로도 이슈를 해결하는 과정에서 알아낸 개념들을 녹여 트러블 슈팅 글로 작성해보려한다.

 

+) 추후에는 컨트롤러의 @PathVariable로 memberId를 찾아오는 것이 아니라, 커스텀 어노테이션을 만들어 JWT 토큰 발급 받은 member를 검증하도록 하였기에 코드가 변경되었다. 커스텀 어노테이션을 만들고, 검증하는 과정은 아직 어려운 점이 많기에 추후 글로 정리해보아야겠다.