뭐가 이렇게 어렵냐?
- 인가와 권한 기반 접근 제어의 개념
- 역할 기반 접근 제어(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, 역할 기반 엑세스 제어를 선택하는 이유
- 관리 용이 : 사용자 수가 많아도 역할 단위로 권한을 변경하기만 하면 해결된다
- 일관성 : 비슷한 사용자들에게 동일 정책을 적용하기 쉬움
- 감사 용이 : 이 역할은 어떤 권한을 갖고 있는가?? - > 에디터는 읽기, 쓰기 같이 빠르게 답할 수 있음
* ACL은 리소스 주체별 목록, ABAC는 속성 기반(시간, 부서, 위치 등)
나는 이번 Spring 학습 과정에서 RBAC 활용에 집중함
권한 설계와 구현방법
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");
}
}
* 흐름 정리 다이어그램
사용자가 컨트롤러에 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에서 일괄 검사하기(중복 로깅 방지)
'Spring Boot' 카테고리의 다른 글
| 인가/권한 관리 : 세션/토큰 기반 인증에서의 인가 구현 (0) | 2025.10.12 |
|---|---|
| 인가/권한 관리 : 인가 구현 예시 (0) | 2025.10.12 |
| 인가와 권한 관리 : 인가(Authorization) (1) | 2025.10.04 |
| 헤더/토큰 기반 인증 : Refresh Token (0) | 2025.10.04 |
| 헤더/토큰 기반 인증 : JWT(JSON Web Token) (0) | 2025.10.04 |