본문 바로가기

Spring Boot

인가/권한 관리 : 권한 기반 접근 제어

뭐가 이렇게 어렵냐?

 

 - 인가와 권한 기반 접근 제어의 개념

 - 역할 기반 접근 제어(RBAC)의 구성 요소(사용자, 역할, 권한, 리소스, 행위)

 - 예제 : 작은 앱에서의 프레임워크 비종속(Java), 권한 설계 및 구현 방법

 - 권한 검증(Authorize) 플로우를 PEP/PDP 관점과 머메이드 다이어그램??으로 이해하기

 - 최소 권한, 기본 거부, 로깅 및 감사 등 실무 팁

 

권한 기반 접근 제어

"인증된 주체가 어떤 리소스에 어떤 행위를 할 수 있는가"를 규칙으로 명시하고, 요청시 규칙에 따라 허용/거부를 결정하는 체계

 

1. 용어 정리

용어 의미 예시
사용자(User) 시스템을 이용하는 주체 alice, bob, ...
역할(Role) 사용자의 업무 묶음 ADMIN, DEITOR, VIEWER
권한(Permission) 특정 리소스에 대한 행위 허용 규칙 POST:READ, POST:WRITE(읽기, 쓰기)
리소스(Resource) 보호 대상 게시글(Post), 파일(File), 주문(Order)
행위(Action) 리소스에 대한 조작 READ, CREATE, UPDATE, DELETE
정책(Policy) 조건이 있는 권한 규칙 "본인 소유 글만 UPDATE 허용"

 

 

2. RBAC(역할 기반 엑세스 제어)의 핵심

RBAC는 사용자 < - > 역할 < - > 권한을 맵핑해준다.. 사용자는 역할을 부여받고, 역할에 권한을 부여해줌

RBAC, 역할기반의 핵심

 

엘리스는 에디터라는 역할을 부여받고, 에디터라는 역할은 읽기, 쓰기를 할 수 있다

또한 엘리스는 뷰어라는 역할을 부여받고, 뷰어라는 역할은 읽기만 할 수 있다

 

* RBAC, 역할 기반 엑세스 제어를 선택하는 이유

 - 관리 용이 : 사용자 수가 많아도 역할 단위로 권한을 변경하기만 하면 해결된다

 - 일관성 : 비슷한 사용자들에게 동일 정책을 적용하기 쉬움

 - 감사 용이 : 이 역할은 어떤 권한을 갖고 있는가?? - > 에디터는 읽기, 쓰기 같이 빠르게 답할 수 있음

 

* ACL은 리소스 주체별 목록, ABAC는 속성 기반(시간, 부서, 위치 등)

나는 이번 Spring 학습 과정에서 RBAC 활용에 집중함

https://soojae.tistory.com/78

 

 

권한 설계와 구현방법

1. 권한 설계의 5대 원칙

원칙 설명 체크 질문?
최소 권한(Latest Privilege) 필요한 최소 권한만 부여함 이 역할이 없어도 업무가 가능함?
기본 거부(Default Deny) 명시적으로 허용된것만 허용함 정책이 없을 때 기본값은 거부인가?
책임 분리(Separation of Duties) 서로 충돌 가능한 권한을 분리함 생성과 승인 권한이 같은 사람에게 있지 않나?
감사 가능성(Auditability) 누가/무엇을/왜 했는지 추적 가능해야함 로그에 정책 키와 사유가 남는가?
단순성 우선(Keep it Simple) 정책은 이해, 검토, 운영이 쉬어야함 정책문서만 보고도 신입이 이해할 수 있나?

 

* 권한은 추가보다 회수가 어렵기 때문에, 초기에 보수적으로 설계하고 필요한 경우 점진적으로 허용범위를 넓힌다.

 

 

2. 도메인 인벤토리 : 리소스, 행위 목록화

권한 설계의 시작은 도메인을 리소스와 행위로 쪼개는 것이라고 한다..

#예시 도메인 인벤토리(서비스 중립)
리소스(Resource) : POST, COMENT, FILE, ORDER
행위(Action) : READ, CREATE, UPDATE, DELETE, EXPORT
상태(State) : DRAFT, PUBLISHED, ARCHIVED, DELETED
소유(Ownership) : OWNER, TEAM, ORG, PUBLIC

 

리소스 핵심 속성 민감도 비고
POST ownerId, status 중간 소유자의 개념이 중요함
FILE ownerId, size, type 높음 다운로드/외부공유를 고려함
ORDER buyerId, amount, state 높음 정산, 환불 플로우가 존재함

 

* 누가(주체) 어떤 리소스에 어떤 행위를, 어떤 조건(상태/소유/시간/위치)에서 하는가?

 

 

3. 역할(Role) 도출 방법

 1) 업무 단위로 묶기 : 실사용자의 업무 시나리오에서 공통 행위를 묶어서 역할 후보를 만듬

 2) 최소 공통권한과 확장 권한을 분리함

 3) 충돌 권한 분리 : 생성자와 승인자를 분리하는 등 SoD(책임 분리, Separation of Duties)를 적용함

사용자 페르소나 주요 목표 역할 후보
콘텐츠 작성자 초안 작성, 수정 EDITOR
검수자 퍼블리시 승인, 회수 REVIEWER
열람자 읽기 전용 VIEWER
운영관리 정책, 역할 관리 ADMIN

 

* 역할 수는 작게 시작하고, 필요시 세분화함(과도한 역할은 운영 복잡도가 급증한다)

 

 

4. 권한 매트릭스 작성(Roles x Resources x Actions)

가장 중요한 부분(산출물)임.

개념을 보여주기 위한 축약 버전

역할\리소스:행위 POST\:READ POST\:CREATE POST\:UPDATE POST\:DELETE FILE\:EXPORT
VIEWER O(공개만) X X X X
EDITOR O O O(소유자만) X X
REVIEWER O X O(승인 전 검수) O(규정에 한함) X
ADMIN O O O O O
역할\리소스:행위 POST\:READ POST\:CREATE POST\:UPDATE POST\:DELETE FILE\:EXPORT
일반회원 O O O(내것만) O(내것만) X
관리자 O O O O O

 

 

 

5. 정책(Policy) 조건 설계 : RBAC 한계를 보완하기

* 아래는 코드가 아닌 정책 문장임.. if같은 조건문이나 쿼리문에 들어가는 것 같음

 - 소유 기반 : "POST.UPDATE는 userId == ownerId일 때만 허용함"

 - 상태 기반 : "POST.DELETE는 status == DRAFT일 때만 허용함"

 - 시간/위치 기반(선택) : "EXPORT는 근무시간(09~18) 내 사내망에서만 허용함"

 - 리스크 기반(선택) : "고액의 ORDER.REFUND는 2인 이상의 승인이 필요하다"

이런식으로 설계할 수 있나보다..

정책은 역할 권한을 필터링하거나 추가 제약을 주는 형태로 동작한다.

 

 

6. 정책 문서화 포맷(예시)

실무에서는 사람이 읽고 리뷰하기 쉬운 포맷이 권장된다고 한다. 유지보수 해야하니까!

# policies/post.yaml
resource: POST
rules:
  - id: post_update_owner_only
    when:
      action: UPDATE
      role: [EDITOR, REVIEWER]
    condition:
      expression: user.id == resource.ownerId # 위의 조건1
    effect: PERMIT
    
  - id: post_delete_only_draft
    when:
      action: DELETE
      role: [REVIEWER, ADMIN]
    condition:
      expression: resource.status == 'DRAFT' # 조건 2
    effect: PERMIT
    
  - id: tenant_isolation
    when:
      action: [READ, CREATE, UPDATE, DELETE]
      role: [*]
    condition:
      expression: user.tenantId == resource.tenantId # 조건 3
    effect: PERMIT
    
  - id: default_deny
    effect: DENY # 위의 규칙에 매칭되지 않으면 거부된다

 

* 사람이 리뷰 가능해야하며, 정책 키(id)는 로그/감사와 연결된다고 함

 

 

7. 권한 검증 플로우

권한 검증 플로우

 


* Spring Security로 구현한 예시 코드

1. 도메인 모델

1) Role 

// domain/Role.java

@Entity
public class Role {
	@Id
    private String name; // "ROLE_DEITOR", "ROLE_VIEWER"
    
    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> permissions = new HashSet<>();
    
    public Role() {
    }
    public Role(String name) { // setter
    	this.name = name;
    }
    
    public String getName() {
    	return name;
    }
    public String getPermissions() {
    	return permissions;
    }
}

 

2) User

// domain/User.java

@Entity
public class User {
	private String username;
    
    private String password;
    
    @ManyToMany(fetch = FetchType.EAGER);
    private Set<Role> roles = new HashSet<>();
    
    public User() {
    }
    public User(String username, String password) { // setter
    	this.username = username;
        this.password = password;
    }
    
    public String getUsername() {
    	return username;
    }
    public String getPassword() {
    	return password;
    }
    public Set<Role> getRoles() {
    	return roles;
    }
}

 

3) Spring Security UserDetails 변환

// security/UserPrincipal.java

public class UserPrincipal implements UserDetails {
	private final User user;
    
    public UserPrincipal(User user) { // Setter
    	this.user = user;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    	return user.getRoles().stream() // return의 stream 과정을 알아야함.. 모르겠음
          .flatMap(role -> role.getPermissions().stream())
          .map(SimpleGrantedAuthority::new)
          .toList();
    }
    
    @Override public String getPassword() { return user.getPassword(); }
    @Override public String getUsername() { return user.getUsername(); }
    @Override public boolean isAccountNotExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

 

4) 정책 조건 적용

Spring Security에서는 MethodSecurityExpressionHandler 또는 @PreAuthorize의 커스텀 SpEL 함수로 정책 구현 가능

 

// security/CustomPermissionEvaluator.java, 정책 조건을 적용하기

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
	
    @Override
    public boolean hasPermission(Authentication authentication, // 인증
                                 Object targetDomainObject,
                                 Object permission) {
        if(targetDomainObject instanceof Post post && "UPDATE".equals(permission)) {
          String currentUser = authentication.getName();
          return post.getOwner().equals(currentUser); // 소유자만 수정 가능, true 반환
        }
        return false;
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, // 권한이 있음
                                 Serializable targetId,
                                 String targetType,
                                 Object permission) {
      // targetId를 통해 ID기반 정책도 구현할 수 있음
      return false;
    }
}

 

 

5)Security 설정

// security/SecurityConfig.java

@EnableWebSecurity
@EnableMethodSecurity // @PreAuthorize 사용 가능하게 해주는 애너테이션
publicc class SecurityConfig {
	
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	return http
               .authorizeHttpRequests(auth -> auth
                 .requestMatchers("/posts/**").authenticated()
                 .anyRequest().permitAll()
               )
               .formLogin(Customizer.withDefaults())
               .build();
    }
    
    @Bean
    public MethodSecurityExpressionHandler expressionHandler(
                       PermissionEvaluator permissionEvaluator) {
      DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
      handler.setPermissionEvaluator(permissionEvaluator);
      return handler;
    }
}

 

 

6) Controller/Service에서 인가 적용

// domain/Post.java (예시)

public class Post {
    @Id
    @Getter
    @Setter
    @GeneratedValue
	private Long id;
    private String owner;
    private String content;
}
// controller/PostController.java
@RestController
@RequestMapping("/posts")
public class PostController {
	private final PostRepository postRepository;
    
    public PostController(PostRepository postRepository) { // setter
    	this.postRepository = postRepository;
    }
    
    @PreAuthorize("hasPermission(#post, 'UPDATE')")
    @PutMapping("/{id}")
    public ResponseEntity<String> updatePost(@PathVariable Long id,
                                             @RequestBody Post updated) {
      Post post = postRepository.findById(id) // 여기서 post를 찾음
                                .orElseThrow(() -> new RuntimeException("Post not found"));
      post.setContent(updated.getContent());
      PostRepository.save(post);
      return ResponseEntity.ok("Updated");
    }
}

 

 

* 흐름 정리 다이어그램

Spring Security 다이어그램

 

사용자가 컨트롤러에 Request 넣음 - > Controller가 Security에 넘겨서 인가 검사

- > Security가 이에 대해 권한이 있는지의 여부를 CustomPermissionEvaluator에 넘겨서 검사함

- > CustomPermissionEvaluator가 Security에게 결과를 넘겨줌(Boolean)

- > Security가 Controller에게 응답해줌

- > Controller가 사용자에게 결과를 반환해줌

 

 

* 기존의 AuthorizationService + PolicyDecisionPoint를 @PreAuthorize + PermissionEvaluator 구조로 바꿀 수 있음

* RBAC(Role + Permission)은 Spring Security의 GrantedAuthority로 표현됨

* 정책 조건(소유자 여부 등)은 PermissionEvaluator에서 커스텀 검증으로 해결한다고 함

// domain/Action.java
public enum Action {
	READ, CREATE, UPDATE, DELETE
}

 

// domain/ResourceType.java
public enum ResourceType {
	POST, FILE, ORDER
}

 

// domain/Permission.java
public class Permission {
	private final ResourceType resource; // enum
    private final Action action; // enum
    
    public Permission(ResourceType resource, Action action) {
    	this.resource = resource;
        this.action = action;
    }
    public Resourcetype resource() {
    	return resource;
    }
    public Action action() {
    	return action;
    }
    
    @Override // override??
    public boolean equals(Object o) {
    	if(!(o instanceof Permission)) { // o의 타입이 Permission이라면
          return false;
        }
        Permission p = (Permission)o;
        return p.resource == resource && p.action == action; // boolean 타입 반환
    } // if 끝
    @Override // override
    public int hashcode() {
      return Objects.hash(resource, action);
    }
    @Override // override
    public String toString() {
      return resource + ":" + action;
    }
}

 

//domain/Role.java

public class Role {
	private final String name;
    private final Set<Permission> permissions = new HashSet<>();
    
    public Role(String name) {
    	this.name = name;
    }
    public String name() {
    	return name;
    }
    
    public Role grant(Permission p) { // 권한 부여
    	permissions.add(p); // 위의 HashSet
        return this;
    }
   	
    public boolean allows(Permission p) { // 권한이 있는지 확인하고 허가해주는 메서드
    	return permissions.contains(p);
    }
    
    public Set<Permission> permissions() {
    	return Collections.unmodifiableSet(Permissions);
    }
}

 

// domain/User.java
public class User {
	private final id;
    private final Set<Role> roles = new HashSet<>();
    
    public User(String id) {
    	this.id = id;
    }
    public String id() {
    	return id;
    }
    
    public User assign(Role r) {
    	roles.add(r);
        return this;
    }
    
    public Set<Role> roles() {
    	return Collections.unmodifiableSet(roles);
    }
}

 

Spring Security 예제 끝!

 

 

 

권한 검증 플로우

1. 요청 처리 흐름

권한 검증 워크플로우

 

권한 검증 워크플로우, 반드시 인증 후에 인가가 이뤄져야한다.

 

 

 

* 최소 권한 원칙(PoLP) : 기본은 읽기만, 쓰기는 필요한 권한에만 부여해주기

* 권한 명세서를 산출물로 관리하기(스프레드시트 등..) : 역할 < - > 권한 목록, 변경이력, 승인자, 등등..

* 감사/로깅 : 403 발생시 정책 키, 리소스 ID, 사용자 ID를 함께 로그 형태 등으로 남겨서 분석 가능하게 하기

* 성능 : 역할 권한은 캐시, 정책 판단은 조건만 캐시(과도한 캐시 금지)

* 국소성 : 컨트롤러, 서비스 초입의 단일 PEP에서 일괄 검사하기(중복 로깅 방지)