권한 모델링: 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 현실 설정
'Java & Spring' 카테고리의 다른 글
| 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway) (0) | 2025.10.15 |
|---|---|
| CORS·XSS·헤더 보안: SPA/REST 현실 설정 (0) | 2025.10.14 |
| OAuth2 로그인(Google/GitHub) + JWT 브릿지 (0) | 2025.10.13 |
| JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략 (0) | 2025.10.13 |
| 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리 (0) | 2025.10.12 |