- 도메인 객체 수준 인가(Authorization) 개념
- 객체 단위에서 접근 제어가 필요한 이유
- 소유권 기반 접근 제어를 구현하는 방법
- 커스텀 Evaluator 를 만들어서 ACL(Access Control List)를 적용하는 방법
- 실무에서 ACL을 적용할 때 고려해야 할 보안 및 유지보수 포인트들
도메인 객체 수준 인가(Access Control List, ACL) 개요
URL 기반이나 메서드 기반 인가에서는 주로 요청 경로(URL)나 메서드 실행 전후(메서드)를 기준으로 한다
도메인 객체 보안(ACL) 은 객체 자체의 속성을 기준으로 접근을 제어한다
가령, 사용자가 특정 문서를 조회하려 할 때 그 문서의 소유자(owner) 가 본인인지 확인하는 방식임
예시
- 블로그 게시글 수정은 작성자 본인만 가능
- 은행 계좌 조회는 계좌 주인만 가능
- 팀 프로젝트 관리 시스템에서 팀원만 프로젝트 열람 가능
| 구분 | 설명 |
| URL 기반 인가 | 경로 단위 제어 |
| 메서드 기반 인가 | 메서드 단위 제어 |
| 객체 기반 인가(ACL) | 도메인 객체 단위 제어 |
소유권 기반 접근 제어
가장 흔한 ACL 패턴은 소유권 기반(Owner-Based) 제어이다
1. 예
@Service
public class DocumentService {
private final DocumentRepository documentRepository;
DocumentService(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
@PreAuthorize("#document.id == authentication.name") // 여기
public Document updateDocument(Document document, String title, String content) {
// 문서 수정 로직 작성
}
}
- #document.owner : 메서드 파라미터로 전달된 문서의ㅏ 소유자
- authentication.name : 현재 로그인한 사용자의 이름
- 두 값이 같을 때에만 수정을 허용한다
2. DB 조회 후 검사하기
@Service
public class DocumentService {
private final DocumentRepository documentRepository;
DocumentService(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
// 커스텀 빈을 따로 만들어서 검사하도록 분리할 수 있음
@PreAuthorize("@documentPermissionEvaluator.isOwner(#id, authentication)")
public Document getDocument(Long id) {
return documentRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Document not found"));
}
}
documentPermissionEvaluator 커스텀 빈을 만들어서 권한 여부를 검사하도록 분리할 수 있다
서비스 로직에서 검증이 빠지게 되는 셈
팁
- 객체의 소유자(owner) 필드를 기준으로 접근을 제어하는 방식이 가장 직관적이다
- 비즈니스 로직과 보안 로직을 분리하려면 별도의 Evaluator 클래스를 두는게 좋다고 함
커스텀 Evaluator 구현하기
Spring Security 에선 PermissionEvaluator 인터페이스를 구현하여 객체 단위 보안을 커스터마이징 할 수 있다
1. PermissionEvaluator 인터페이스
여기에 있다
package org.springframework.security.access;
import java.io.Serializable;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.security.core.Authentication;
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);
}
- 첫번째 hasPermission 메서드 : 객체 자체를 전달받아서 권한 확인(targetDomainObject)
- 두번째 hasPermission 메서드 : ID와 타입으로 객체를 식별하여 권한을 확인(targetId, targetType)
기본적으로 PermissionEvaluator 는 DenyAllPermissionEvaluator 를 구현하고 있다
package org.springframework.security.access.expression;
import java.io.Serializable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
public class DenyAllPermissionEvaluator implements PermissionEvaluator {
private final Log logger = LogFactory.getLog(this.getClass());
public DenyAllPermissionEvaluator() {
}
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
this.logger.warn(LogMessage.format("Denying user %s permission '%s' on object %s", authentication.getName(), permission, target));
return false;
}
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
this.logger.warn(LogMessage.format("Denying user %s permission '%s' on object with Id %s", authentication.getName(), permission, targetId));
return false;
}
}
2. 커스텀 구현 예
import com.b1uffer.multisessiontest.entity.Document;
import com.b1uffer.multisessiontest.repository.DocumentRepository;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import java.io.Serializable;
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository repository;
public CustomPermissionEvaluator(DocumentRepository repository) {
this.repository = repository;
}
// 객체 기반 검사만 하는 메서드
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
if(targetDomainObject instanceof Document document) {
String username = authentication.getName();
return document.getOwner().equals(username);
}
return false;
}
// ID 기반 검사
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
Long id = Long.valueOf(targetId.toString());
Document document = repository.findById(id)
.orElseThrow(() -> new NoSuchElementException("여기서 터져버림"));
String username = authentication.getName();
return document.getOwner().equals(username);
}
}
3. SpEL에서 위 커스텀을 사용하기
@PreAuthorize("hasPermission(#document, 'write')") // 여기서 검증 끝
public void editDocument(Document document) {
Document editDocument = documentRepository.findById(document.getId())
.orElseThrow(() -> new IllegalArgumentException("Document not found"));
editDocument.setTitle(document.getTitle());
editDocument.setContent(document.getContent());
documentRepository.save(editDocument);
}
- hasPermission(#document, 'write') - > CustomPermissionEvaluator 가 호출됨
hasPermission 메서드는 이걸 불러온거임
public boolean hasPermission(Object target, Object permission) {
return this.permissionEvaluator.hasPermission(this.getAuthentication(), target, permission);
}
그래서 CustomPermissionEvaluator 에는
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
매개변수로 authentication 이 주입되어 있지만, 실제로 우리가 SpEL을 통해 사용하는 메서드에는 매개변수가 target, permission
두 개 들어간다
- 사용자와 문서 소유자가 일치해야 true를 반환함
팁
- Evaluator 를 구현하면 재사용성이 높아지고, 여러 객체 타입에 대한 보안 로직을 한곳에서 관리할 수 있다
- DB 조회를 포함하는 Evaluator 는 성능에 유의해야한다
| 항목 | 설명 |
| PermissionEvaluator | 객체 단위 인가를 위한 인터페이스 |
| 커스텀 구현 | 소유자 확인, DB 조회 기반 권한 확인 |
| SpEL 통합 | hasPermission() 을 통해 사용 |
'Spring Boot > Security 쿠키,세션 기반 인증,인가' 카테고리의 다른 글
| 세션 기반 사용자 인가 구현 : 메서드 보안을 통한 인가 (0) | 2026.05.15 |
|---|---|
| 세션 기반 사용자 인가 구현 : URL 패턴 기반 인가 설정 (0) | 2026.05.15 |
| 세션 기반 사용자 인가 구현 : 인증과 인가의 관계 (0) | 2026.05.14 |
| Remember-Me : 보안 (0) | 2026.05.14 |
| Remember-Me : 설정 (0) | 2026.05.12 |