티스토리 뷰

반응형

스프링 시큐리티 완전 처음부터 5편 (Kotlin)

ROLE 기반 권한 제어 + @PreAuthorize 실전 사용 (이제 ‘보안 설계’를 한다)


여기까지 왔다면, 솔직히 말해서
로그인/인증은 이미 끝났다.

  • JWT 발급 ✔
  • JWT 검증 필터 ✔
  • Stateless 구조 ✔

이제 남은 진짜 실무 포인트는 이거다.

“누가 이 API를 호출할 수 있는가?”

로그인만 되면 다 접근 가능?
→ 그건 보안이 아니다. 그냥 인증일 뿐이다.

이번 글에서는:

  • ROLE 기반 권한 설계
  • URL 단위 권한 제어
  • 메서드 단위 권한 제어 (@PreAuthorize)
  • 실무에서 절대 하면 안 되는 실수들

까지 한 번에 정리한다.

이 편이 끝나면
“시큐리티는 이제 구조적으로 이해한다”
라고 말해도 된다.


✔ 이번 글에서 만들 최종 그림

ROLE_USER  → 일반 사용자
ROLE_ADMIN → 관리자

API권한

GET /public/** 모두
GET /user/** ROLE_USER
GET /admin/** ROLE_ADMIN

그리고 코드 레벨에서는:

@PreAuthorize("hasRole('ADMIN')")
fun adminOnly()

이걸 실제로 동작하게 만든다.


0. 스프링 시큐리티에서 ROLE 개념, 헷갈리는 포인트부터 정리

❗ 제일 많이 헷갈리는 것

  • DB에는 USER, ADMIN 저장
  • 시큐리티 내부에서는 반드시 ROLE_ prefix

즉,

DB 값시큐리티 권한

USER ROLE_USER
ADMIN ROLE_ADMIN

이건 규칙이다. 외우자.

hasRole("ADMIN")
→ 내부적으로는 ROLE_ADMIN을 찾는다


1. User Entity에 role 이미 있다고 가정 (복습)

반응형

2편에서 만들었던 User Entity 기억날 거다.

@Entity
@Table(name = "users")
class User(
    @Column(nullable = false, unique = true)
    val username: String,

    @Column(nullable = false)
    val password: String,

    @Column(nullable = false)
    val role: String = "USER",

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
)

여기서 중요한 건 딱 하나.

role 컬럼에는 USER, ADMIN 처럼 prefix 없는 값만 저장


2. CustomUserDetails에서 권한 매핑 다시 보기 (중요)

시큐리티는 UserDetails.getAuthorities() 결과만 본다.
그래서 여기서 ROLE prefix를 붙여줘야 한다.

override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
    return mutableListOf(
        SimpleGrantedAuthority("ROLE_${user.role}")
    )
}

이 한 줄 덕분에:

  • DB: ADMIN
  • 시큐리티 내부: ROLE_ADMIN

으로 인식된다.

👉 이게 빠지면
hasRole("ADMIN") 은 무조건 실패한다.


3. URL 기반 권한 제어 (가장 직관적인 방식)

먼저 SecurityConfig 에서 URL 기준으로 나눈다.


3-1. SecurityConfig – URL 권한 설정

.authorizeHttpRequests {
    it.requestMatchers("/public/**").permitAll()
    it.requestMatchers("/user/**").hasRole("USER")
    it.requestMatchers("/admin/**").hasRole("ADMIN")
    it.anyRequest().authenticated()
}

이 설정을 말로 풀면 이렇다.

  • /public/**
    → 로그인 없이 접근 가능
  • /user/**
    → ROLE_USER 이상
  • /admin/**
    → ROLE_ADMIN 만 가능
  • 그 외
    → 로그인은 필요

📌 주의
hasRole("USER") 는
ROLE_USER 가 있는지만 본다.


3-2. 테스트용 컨트롤러 만들기

📄 RoleTestController.kt

@RestController
@RequestMapping
class RoleTestController {

    @GetMapping("/public/hello")
    fun publicApi() = "public ok"

    @GetMapping("/user/hello")
    fun userApi() = "user ok"

    @GetMapping("/admin/hello")
    fun adminApi() = "admin ok"
}

3-3. 테스트 시나리오

로그인 유저/user/admin

ROLE_USER ❌ (403)
ROLE_ADMIN

👉 403 Forbidden 이 나오면 정상이다.
(401 은 인증 실패, 403 은 권한 부족)


4. 메서드 레벨 권한 제어 – @PreAuthorize (실무에서 더 많이 씀)

URL 기준 제어는 단순하지만,
실무에서는 이런 요구가 많다.

  • “같은 URL인데, 조건에 따라 접근 허용”
  • “서비스 로직 단위로 권한 체크”
  • “컨트롤러 말고 서비스 레벨에서 막고 싶다”

그래서 쓰는 게 @PreAuthorize 다.


4-1. 메서드 보안 활성화 (필수)

이거 안 하면 아무리 @PreAuthorize 써도 동작 안 한다.

@EnableMethodSecurity(prePostEnabled = true)
@Configuration
class MethodSecurityConfig

📌 Spring Boot 3.x 기준
@EnableGlobalMethodSecurity ❌
@EnableMethodSecurity ⭕


4-2. @PreAuthorize 기본 예제

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin-only")
fun adminOnlyApi(): String {
    return "관리자만 접근 가능"
}

이 한 줄의 의미:

“이 메서드는 ROLE_ADMIN 이 있는 사용자만 실행 가능”


4-3. 서비스 레벨에서 권한 제어 (실무 추천)

컨트롤러 말고
Service 레벨에서 막는 게 더 안전하다.

@Service
class AdminService {

    @PreAuthorize("hasRole('ADMIN')")
    fun deleteUser(userId: Long) {
        // 진짜 위험한 로직
    }
}

👉 이유?

  • 컨트롤러는 바뀌기 쉽다
  • 서비스는 핵심 비즈니스 로직이다
  • 실수로 다른 컨트롤러에서 호출해도 권한이 자동으로 막힌다

5. 표현식 조금만 더 (실무에서 자주 쓰는 것들)

✔ 여러 권한 허용

@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")

✔ 인증된 사용자만

@PreAuthorize("isAuthenticated()")

✔ 본인 데이터만 접근

@PreAuthorize("#userId == authentication.name")

📌 이건 JWT payload에 userId 를 넣는 구조에서 자주 쓴다.


6. 실무에서 진짜 많이 터지는 사고들

❌ ROLE prefix 직접 비교

hasAuthority("ADMIN") // 실패

👉 항상 ROLE_ADMIN


❌ 컨트롤러에서만 권한 체크

  • 서비스에 권한 체크 없음
  • 다른 API에서 실수로 호출 → 보안 사고

❌ JWT payload에 너무 많은 정보 저장

  • role 하나면 충분
  • 개인정보 ❌
  • 민감 정보 ❌

JWT는 서명된 공개 데이터다.


7. 이 시리즈를 여기까지 따라왔다면

이제 이런 말을 해도 된다.

  • “JWT 기반 인증/인가 구조 구현해봤습니다”
  • “Spring Security 필터 체인 이해하고 있습니다”
  • “ROLE 기반 권한 설계 경험 있습니다”
  • “@PreAuthorize 사용 가능합니다”

이건 주니어 레벨 답변이 아니다.


 


출처

  • Spring Security 공식 문서
  • Spring Boot 3.x Reference
  • 실무 Kotlin + JWT 기반 인증 서버 운영 경험

 

스프링시큐리티, springsecurity, 코틀린, kotlin, jwt, rolebasedaccess, preauthorize, 권한관리, 백엔드개발, 개발블로그

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함
반응형