본문 바로가기

Spring Boot/Security 쿠키,세션 기반 인증,인가

세션 기반 사용자 인가 구현 : 도메인 객체 보안(ACL)

 - 도메인 객체 수준 인가(Authorization) 개념

 - 객체 단위에서 접근 제어가 필요한 이유

 - 소유권 기반 접근 제어를 구현하는 방법

 - 커스텀 Evaluator 를 만들어서 ACL(Access Control List)를 적용하는 방법

 - 실무에서 ACL을 적용할 때 고려해야 할 보안 및 유지보수 포인트들


도메인 객체 수준 인가(Access Control List, ACL) 개요

URL 기반이메서드 기반 인가에서는 주로 요청 경로(URL)메서드 실행 전후(메서드)를 기준으로 한다

도메인 객체 보안(ACL) 은 객체 자체의 속성을 기준으로 접근을 제어한다

 

가령, 사용자가 특정 문서를 조회하려 할 때 그 문서의 소유자(owner) 가 본인인지 확인하는 방식임

ACL(Access Control List) 기반 인가

 

 

예시

  • 블로그 게시글 수정은 작성자 본인만 가능
  • 은행 계좌 조회는 계좌 주인만 가능
  • 팀 프로젝트 관리 시스템에서 팀원만 프로젝트 열람 가능

 

구분 설명
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() 을 통해 사용