스프링 시큐리티: 역할(Role)과 권한(Authority)
hasRole("ADMIN")과hasAuthority("ADMIN")은 같은 동작을 할까요?ROLE_ADMIN과ADMIN은 어떻게 다를까요?
Spring Security에서 Role과 Authority는 모두 GrantedAuthority 인터페이스로 표현되지만, ROLE_ 접두사 규칙 때문에 사용 방식이 다릅니다. 이 차이를 모르면 권한 체크가 의도대로 동작하지 않는 상황을 겪게 됩니다.
개념 정의
Authority(권한) 는 사용자가 수행할 수 있는 개별 동작을 나타내는 문자열입니다. Spring Security에서 모든 권한은 GrantedAuthority 인터페이스로 표현됩니다.
Role(역할) 은 Authority의 특수한 형태로, ROLE_ 접두사가 붙은 Authority입니다. Spring Security는 Role을 다룰 때 내부적으로 ROLE_ 접두사를 자동으로 관리합니다.
Authority: READ_PRIVILEGE, WRITE_PRIVILEGE, DELETE_PRIVILEGE
Role: ROLE_USER, ROLE_ADMIN, ROLE_MANAGER
hasRole()과 hasAuthority()의 차이
핵심 차이는 ROLE_ 접두사 자동 추가 여부 입니다.
| 메서드 | 전달값 | 내부 비교값 |
|---|---|---|
hasRole("ADMIN") | "ADMIN" | "ROLE_ADMIN" (자동 추가) |
hasAuthority("ADMIN") | "ADMIN" | "ADMIN" (그대로) |
hasAuthority("ROLE_ADMIN") | "ROLE_ADMIN" | "ROLE_ADMIN" (그대로) |
hasRole("ADMIN")은 내부적으로 ROLE_ADMIN을 찾습니다. DB에 ADMIN으로 저장했다면 hasRole("ADMIN")은 실패합니다.
URL 기반 인가 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN 확인
.requestMatchers("/api/posts/**").hasAuthority("WRITE_POST") // WRITE_POST 확인
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
메서드 기반 인가 설정
@EnableMethodSecurity를 활성화하면 메서드 레벨에서도 권한을 체크할 수 있습니다.
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig { }
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) { ... }
@PreAuthorize("hasAuthority('WRITE_POST') and #authorId == authentication.name")
public void editPost(Long postId, String authorId) { ... }
@PreAuthorize는 SpEL(Spring Expression Language)을 지원하므로, 역할 체크와 파라미터 기반 조건을 조합할 수 있습니다.
Role과 Authority를 함께 사용하는 구조
실무에서는 Role(역할)로 큰 범위를 나누고, Authority(세부 권한)로 세밀한 접근 제어를 합니다.
ROLE_ADMIN → READ_POST, WRITE_POST, DELETE_POST, MANAGE_USER
ROLE_USER → READ_POST, WRITE_POST
ROLE_GUEST → READ_POST
UserDetails에서 이 구조를 반영하려면 getAuthorities()에서 Role과 Authority를 모두 반환합니다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// Role 추가
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.name()));
// 세부 권한 추가
role.getPermissions().forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission))
);
return authorities;
}
주의할 점
hasRole()은 자동으로 ROLE_ 접두사를 붙인다
hasRole("ADMIN")은 내부적으로 ROLE_ADMIN을 찾습니다. DB에 ADMIN으로만 저장했다면 hasAuthority("ADMIN")을 사용하거나, 저장할 때 ROLE_ADMIN으로 넣어야 합니다. 팀 내에서 "Role 저장 시 ROLE_ 접두사를 포함하는지 안 하는지"를 명확하게 규약으로 정해야 합니다.
@RolesAllowed와 @PreAuthorize의 차이
@RolesAllowed는 JSR-250 표준이고 SpEL을 지원하지 않습니다. @PreAuthorize는 SpEL로 복잡한 조건을 표현할 수 있습니다. 한 프로젝트에서 둘을 혼용하면 가독성이 떨어지므로, 팀 내에서 하나만 선택해 일관성을 유지해야 합니다.
roles()와 authorities()를 User.builder()에서 혼용하면 안 된다
// roles()는 자동으로 ROLE_ 접두사를 붙인다
User.builder().roles("USER") // → ROLE_USER
// authorities()는 전달값 그대로 사용
User.builder().authorities("ROLE_USER") // → ROLE_USER
// 둘을 같이 호출하면 나중 호출이 이전 값을 덮어쓴다
User.builder()에서 roles()와 authorities()를 동시에 호출하면, 마지막 호출만 반영됩니다. 하나만 선택해서 사용해야 합니다.
정리
| 항목 | 설명 |
|---|---|
| Authority | GrantedAuthority로 표현되는 개별 권한 문자열 |
| Role | ROLE_ 접두사가 붙은 특수한 Authority |
| hasRole("ADMIN") | 내부적으로 ROLE_ADMIN을 찾음 (접두사 자동 추가) |
| hasAuthority("ADMIN") | ADMIN을 그대로 찾음 (접두사 추가 안 함) |
| 실무 패턴 | Role로 큰 범위 구분, Authority로 세밀한 접근 제어 |
| 메서드 인가 | @PreAuthorize로 SpEL 기반 조건 체크 가능 |