본문 바로가기
Java & Spring

권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한

by yamoojin83 2025. 10. 14.

권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한

서비스가 커질수록 “누가 무엇을 할 수 있는가”를 명확하게 정의해야 합니다. Spring Security에서는 ROLE_* 기반의 역할(Role)과 임의 문자열 기반의 권한(Authority)을 함께 사용할 수 있습니다. 이 글은 역할=상위 그룹, 권한=세부 행위로 나눠 설계하고, @PreAuthorize도메인 권한(문서 소유자/조직 등)에 맞춰 검증하는 패턴을 정리합니다.

1) 역할 vs 권한: 언제 무엇을 쓰나

  • ROLE: “관리자/매니저/사용자”처럼 사람 그룹. URL 단위 접근 제어에 적합. hasRole("ADMIN")
  • Authority: “게시글:읽기/쓰기/삭제”처럼 구체 행위. 메서드/도메인 제어에 적합. hasAuthority("POST:WRITE")
  • 권장: 큰 틀은 ROLE로, 세부 행위는 Authority로 관리. 혼합 사용.

2) 권한 저장과 매핑


@Entity
@Table(name = "users")
public class User {
  @Id @GeneratedValue Long id;
  @Column(unique = true, nullable = false) String email;
  @Column(nullable = false) String password;
  // 예: "ROLE_ADMIN,POST:READ,POST:WRITE"
  @Column(nullable = false) String authorities;
  boolean enabled = true;
  boolean locked  = false;
}

public class AppUserDetails implements UserDetails {
  private final User user;
  public AppUserDetails(User user) { this.user = user; }

  @Override public Collection<? extends GrantedAuthority> getAuthorities() {
    return Arrays.stream(user.getAuthorities().split(","))
        .map(String::trim).filter(s -> !s.isEmpty())
        .map(SimpleGrantedAuthority::new).toList();
  }
  @Override public String getUsername() { return user.getEmail(); }
  @Override public String getPassword() { return user.getPassword(); }
  @Override public boolean isEnabled()  { return user.isEnabled(); }
  @Override public boolean isAccountNonLocked() { return !user.isLocked(); }
  @Override public boolean isAccountNonExpired() { return true; }
  @Override public boolean isCredentialsNonExpired() { return true; }
}

3) URL 인가 + 메서드 인가


@Configuration
@EnableMethodSecurity // @PreAuthorize 사용
public class SecurityConfig {
  @Bean
  SecurityFilterChain http(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/", "/login", "/css/**").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/posts/**").hasAnyAuthority("POST:READ","POST:WRITE")
        .anyRequest().authenticated())
      .formLogin(Customizer.withDefaults());
    return http.build();
  }
}

@RestController
@RequestMapping("/posts")
public class PostApi {
  private final PostService posts;

  public PostApi(PostService posts) { this.posts = posts; }

  @GetMapping("/{id}")
  @PreAuthorize("hasAuthority('POST:READ')")
  public PostDto read(@PathVariable Long id) { return posts.read(id); }

  @DeleteMapping("/{id}")
  @PreAuthorize("hasAuthority('POST:DELETE')") // 세부 행위 제어
  public void delete(@PathVariable Long id) { posts.delete(id); }
}

4) 도메인 권한: “내 글만 수정 가능” 같은 조건

단순 ROLE/Authority만으로 부족할 때는 도메인 객체 기반 검증이 필요합니다. 가장 간단한 방법은 @PreAuthorize에서 서비스 빈을 호출하는 것입니다.


@Service("perm")
public class PermissionService {
  private final PostRepository posts;
  public PermissionService(PostRepository posts) { this.posts = posts; }

  public boolean canEdit(Long postId, Authentication auth) {
    String me = auth.getName(); // 이메일/ID
    return posts.findById(postId)
        .map(p -> p.getAuthorEmail().equals(me))
        .orElse(false);
  }
}

@PutMapping("/{id}")
@PreAuthorize("@perm.canEdit(#id, authentication)")
public PostDto update(@PathVariable Long id, @RequestBody PostReq req) { ... }

5) PermissionEvaluator로 hasPermission 사용

SpEL을 단순화하려면 PermissionEvaluator를 등록하고 hasPermission(target, 'EDIT')처럼 쓸 수 있습니다.


@Component
public class PostPermissionEvaluator implements PermissionEvaluator {
  private final PostRepository posts;
  public PostPermissionEvaluator(PostRepository posts){ this.posts = posts; }

  @Override
  public boolean hasPermission(Authentication a, Object targetDomainObject, Object permission) {
    if (targetDomainObject instanceof Post p && "EDIT".equals(permission)) {
      return p.getAuthorEmail().equals(a.getName());
    }
    return false;
  }

  @Override
  public boolean hasPermission(Authentication a, Serializable targetId, String type, Object permission) {
    if ("Post".equals(type) && "EDIT".equals(permission)) {
      return posts.findById((Long) targetId)
          .map(p -> p.getAuthorEmail().equals(a.getName()))
          .orElse(false);
    }
    return false;
  }
}

@Configuration
@EnableMethodSecurity
public class MethodSecurityCfg {

  @Bean
  MethodSecurityExpressionHandler methodSecurityExpressionHandler(PermissionEvaluator pe) {
    DefaultMethodSecurityExpressionHandler h = new DefaultMethodSecurityExpressionHandler();
    h.setPermissionEvaluator(pe);
    return h;
  }
}

// 사용 예시
@PreAuthorize("hasPermission(#post, 'EDIT')")
public void edit(Post post) { ... }

6) Role Hierarchy(상하관계)

“ADMIN > MANAGER > USER”처럼 상하관계가 있으면 Role Hierarchy로 ADMIN이 USER 권한을 암시적으로 포함하게 만들 수 있습니다.


@Configuration
public class RoleHierarchyCfg {

  @Bean
  RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl h = new RoleHierarchyImpl();
    h.setHierarchy("ROLE_ADMIN > ROLE_MANAGER \n ROLE_MANAGER > ROLE_USER");
    return h;
  }

  @Bean
  GrantedAuthoritiesMapper authoritiesMapper(RoleHierarchy h) {
    return new RoleHierarchyAuthoritiesMapper(h);
  }

  @Bean
  AuthenticationProvider dao(UserDetailsService uds, PasswordEncoder pe, GrantedAuthoritiesMapper m) {
    DaoAuthenticationProvider p = new DaoAuthenticationProvider();
    p.setUserDetailsService(uds);
    p.setPasswordEncoder(pe);
    p.setAuthoritiesMapper(m); // 계층 적용
    return p;
  }
}

7) 설계 팁

  • URL은 ROLE 중심, 메서드/도메인은 Authority 중심으로.
  • 권한 문자열 컨벤션 예: 리소스:행위 (예: POST:WRITE, ORDER:CANCEL).
  • 감사 로깅: 권한 거부(403) 시 사용자/리소스/행위 기록.
  • 조직/테넌트 단위 권한은 도메인 권한으로 확장(조직 ID 매칭).

 

 

👉 1편: SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기

👉 2편: PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책

👉 3편: AuthenticationProvider & UserDetailsService 커스터마이징

👉 4편: 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리

👉 5편: JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략

👉 6편: OAuth2 로그인(Google/GitHub) + JWT 브릿지

👉 7편: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한

👉 8편: CORS·XSS·헤더 보안: SPA/REST 현실 설정

👉 9편: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)

👉 10편: 감사 로깅/Audit: 로그인 실패·권한거부 탐지 규격