지금 회사에서 개발하고 있는 서비스는 보안이 매우 중요하고 복잡하다.(Tenant, 회사, 그룹 등 다양한 조건 별로 접근 권한 정책이 매우 복잡하다) 이런 복잡한 권한 문제를 해결할 수 있는 방법 중 하나로 Spring Security ACL이 있는데, 동작 원리를 이해하기 위해서 정리해보고 직접 실습을 해보는 시간을 가졌다.
실습코드 : https://github.com/doongjun/spring-security-acl-tutorial
ACL (Access Control List)
- 객체에 연결된 사용 권한 목록
- acl은 지정된 객체에 대해 어떤 작업을 수행할 것인지 지정한다.
- Spring Security ACL는 도메인 객체 보안을 지원하는 Spring 구성 요소이다.
- 전체 작업이 아닌 단일 도메인 객체에 대한 권한(사용자/역할)을 정의하는데 용이하다.
ex) 공지사항
- 전체 관리자(Admin)는 모든 공지사항을 편집하고 읽을 수 있다.
- 편집자(Editor)는 특정 공지사항에 대해서 편집하고 읽을 수 있다.
- 일반 유저(Normal user)는 공지사항을 읽을 수 있다.
사용자/역할마다 특정 객체에 대한 사용 권한이 다른 경우, Spring ACL을 사용하여 권한 검사를 수행할 수 있다.
ACL database 설정
(이 외 Dependency, ACL 관계 설정은 아래 링크 참고)
ACL을 사용하기 위해서 4개의 테이블이 필요하다.
acl_class : 도메인 객체의 클래스 이름을 저장한다.
- id
- class : 보안 도메인 객체의 클래스 이름 ex) com.baeldung.acl.persistence.entity.NoticeMessage
acl_sid : 시스템에서 모든 원칙이나 권한을 보편적으로 식별할 수 있는 테이블
- id
- sid : 사용자 이름 또는 역할(권한) 이름. SID는 보안 ID를 나타낸다.
- principal : sid가 사용자 이름인지 역할인지 나타낸다. (0 or 1)
acl_object_identity : 각 고유 도메인 객체에 대한 정보를 저장하는 테이블
- id
- object_id_class : 도메인 객체 클래스 정의, links to acl_class table
- object_id_identity : 도메인 객체는 클래스에 따라 많은 테이블에 저장될 수 있다. 따라서 이 필드는 대상 객체 기본키를 저장한다.
- parent_object : 이 객체의 상위 항목 지정
- owner_sid : 객체 소유자의 id, links to acl_sid table
- entries_inheriting : 이 객체의 acl 항목(acl_entry에 정의)이 상위 객체에서 상속되는지 여부
acl_entry : 개별 권한은 객체id의 각 sid에 할당된다
- id
- acl_object_identity : 객체 id 지정, links to acl_object_identity table
- ace_order : 해당 객체 id의 acl 항목 목록에서 현재 항목 순서
- sid : permission이 부여되거나 거부된 대상 sid, links to acl_sid table
- mask : 부여되거나 거부된 실제 permission을 나타내는 integer bit mask
- granting : 1 - 부여됨, 0 - 거부됨
- audit_success and audit_failure : 추적 목적
위 테이블 구조로 어떻게 도메인 객체 보안을 지원할 수 있을까?
게시판을 예로 들어보자.
여러 게시판을 권한에 따라 관리하려면 ACL 테이블에 필요한 정보가 저장되어있어야 한다.
board
id | name |
201 | 공지사항 |
202 | 자유 게시판 |
acl_class
id | class |
1 | com.tutorial.acl.domain.Board |
게시판 도메인 객체를 관리할 것이기 때문에 해당 클래스 이름을 저장한다.
acl_sid
id | sid | principal |
11 | userA | true |
12 | userB | true |
13 | ROLE_EDITOR | false |
userA, userB 사용자와 EDITOR 역할을 저장한다.
principal 컬럼을 통해서 해당 sid가 사용자인지, 역할인지 구분한다.
acl_object_identity
id | object_id_class | object_id_identity | parent_object | owner_sid | entires_inheriting |
110 | 1 | 201 | null | 13 | false |
120 | 1 | 202 | null | 13 | false |
여기서 중요하게 볼 것은 object_id_class, object_id_identity 이다. (관리할 도메인 객체의 정보를 저장하는 테이블이니까)
object_id_class는 acl_class를 바라본다. 해당 객체를 관리할 것이다.
object_id_identity는 관리할 객체의 id이다. 이 예제에서 공지사항(201)과 자유 게시판(202)을 관리할 것이기 때문에 해당 id를 저장한다.
acl_entry
id | acl_object_identity | ace_order | sid | mask | granting | a_s | a_f |
301 | 110 | 1 | 11 | 1 | true | true | true |
acl_entry 테이블에 위 값을 저장하면 공지사항에 대한 읽기권한이 userA에게 부여된다.
여기서 중요하게 볼 것은 acl_object_identity, sid 그리고 mask 이다.
acl_object_identity는 acl_object_identity를 바라본다. 바라본 테이블에는 관리할 도메인의 정보가 있다.
sid는 acl_sid를 바라본다. 바라본 테이블에는 권한을 부여할 대상이 존재한다.(사용자 또는 역할)
mask는 권한의 종류이다. 1은 읽기권한이다.
Test
이제 권한을 부여했으니 Spring Security ACL이 잘 동작하는지 테스트해보자.
1. userA 조회 테스트
@Test
@WithMockUser(username = "userA")
fun `userA_게시판_전체조회`() {
//when
val findAll: List<Board> = boardRepository.findAll()
//then
assertThat(findAll.size).isEqualTo(1)
assertThat(findAll[0].id).isEqualTo(noticeId)
assertThat(findAll[0].name).isEqualTo("공지사항")
}
테스트가 잘 동작할까?
아쉽게도 아니다.
userA가 findAll() 함수로 조회해온 결과의 개수를 1개로 예상했지만 2개가 조회되어 실패했다.
board테이블에는 공지사항과 자유게시판이 저장되어있다. 자유게시판에 권한을 부여한 적이 없는데, 자유게시판이 같이 조회되었으니 권한 설정을 잘못한 것일까?
권한 설정은 잘 했지만, 조회해오는 findAll() 함수에 @PostFilter 라는 애노테이션을 작성해주지 않아서 모두 조회되었다.
@Repository
interface BoardRepository: JpaRepository<Board, Long> {
@PostFilter("hasPermission(filterObject, 'READ')")
override fun findAll(): List<Board>
}
위와 같이 @PostFilter 애노테이션을 작성하면 테스트가 정상적으로 통과하는 것을 볼 수 있다.
위와 같은 flow로 Spring Security ACL이 동작한다.
읽기 권한 뿐만 아니라 쓰기, 다운로드 등 다양한 기본 권한들이 있고 개발자가 커스텀하여 사용할 수 있다.
참고링크
'Web developer > Spring' 카테고리의 다른 글
[JPA] Entity select만 했는데 update쿼리가 실행될 때 (0) | 2023.01.09 |
---|---|
[Spring] 스프링 시큐리티 로그인 기능 구현해보기 (0) | 2021.08.24 |
[Spring] 스프링 시큐리티 이해하기 (0) | 2021.08.21 |
[Spring] 기본 설정 + Spring Security 설정 (0) | 2021.08.20 |
[Spring] MockMVC Test (0) | 2021.08.02 |
댓글