개발가능구역

[ IntelliJ ] Spring Security로 역할 기반 접근 제어(RBAC) 구현하기: 관리자, 일반 회원, 비회원 권한 설정 가이드 본문

SpringBoot/IntelliJ

[ IntelliJ ] Spring Security로 역할 기반 접근 제어(RBAC) 구현하기: 관리자, 일반 회원, 비회원 권한 설정 가이드

oosomall 2025. 10. 8. 15:42
반응형

 

웹 애플리케이션 개발에서 보안은 아무리 강조해도 지나치지 않다. 특히 사용자 역할에 따라 접근 가능한 기능을 제한하는  역할 기반 접근 제어(Role-Based Access Control, RBAC)는 필수적이다.

 

이 글에서는 Spring Security를 이용하여 관리자(ADMIN), 일반 회원(MEMBER), 비회원(ANONYMOUS) 세 가지 주요 역할에 대한 접근 권한을 IntelliJ 기반 Spring Boot 프로젝트에서 설정하는 방법을,

기존 코드에서 바뀐 코드를 중심으로 자세히 설명하고, 앞전에 다뤘던 Security 설정도 다시 복습해 보았다.

 


 

❌ 전: 기존 SecurityConfig 클래스

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
        this.customOAuth2UserService = customOAuth2UserService;
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/sign_up", "/signup_save",
                                "/check/**", "/image/**", "/css/**", "/js/**").permitAll()
                        .anyRequest().authenticated()
                )

                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/go_login")
                        .usernameParameter("id")
                        .passwordParameter("pw")
                        .defaultSuccessUrl("/")
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login")
                        .permitAll()
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .successHandler(customAuthenticationSuccessHandler())
                        //.defaultSuccessUrl("/", true)
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(customOAuth2UserService)
                        )
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

}

 

 


 

 

후: SecurityConfig 클래스

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
        this.customOAuth2UserService = customOAuth2UserService;
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        // 1. 비회원 (모두 접근 가능)
                        .requestMatchers("/", "/sign_up", "/signup_save",
                                "/check/**", "/image/**", "/css/**", "/js/**").permitAll()
                        // 2. 관리자 (ADMIN 역할만 접근 가능
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        // 3. 일반 회원(MEMBER 역할만 접근 가능)
                        .requestMatchers("/member/**").hasRole("MEMBER")
                        // 4. 나머지 모든 요청은 로그인만 하면 접근 가능(인증)
                        // ( 로그인 = ADMIN, MEMBER 모두 해당된다 )
                        .anyRequest().authenticated()
                )
                /* 관리자 외 접근권한 닫기
                1. exceptionHandling : Security가 인증/인가 과정에서 발생하는 예외를 처리하는 방법 설정
                2. accessDeniedPage : 로그인 상태이지만 요청된 리소스에 접근할 권한이 없을 때(403) /login페이지로 이동
                */
                .exceptionHandling(exception -> exception
                        .accessDeniedPage("/login")
                )

                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/go_login")
                        .usernameParameter("id")
                        .passwordParameter("pw")
                        .defaultSuccessUrl("/")
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login")
                        .permitAll()
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .successHandler(customAuthenticationSuccessHandler())
                        //.defaultSuccessUrl("/", true)
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(customOAuth2UserService)
                        )
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

}

 

1. Spring Security 설정 파일 개요

우리가 권한 설정을 할 핵심 파일은 @Configuration과 @EnableWebSecurity 어노테이션이 붙은 SecurityConfig 클래스다. 이 클래스 내의 filterChain(HttpSecurity http) 메서드가 모든 보안 규칙을 정의하는 역할을 한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    // ... (생성자 및 기타 빈 설정 생략)

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        // 1. 비회원 (모두 접근 가능)
                        .requestMatchers("/", "/sign_up", "/signup_save",
                                "/check/**", "/image/**", "/css/**", "/js/**").permitAll()
                        // 2. 관리자 (ADMIN 역할만 접근 가능
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        // 3. 일반 회원(MEMBER 역할만 접근 가능)
                        .requestMatchers("/member/**").hasRole("MEMBER")
                        // 4. 나머지 모든 요청은 로그인만 하면 접근 가능(인증)
                        .anyRequest().authenticated()
                )
                // ... (예외 처리, 폼 로그인, 로그아웃, OAuth2 설정 생략)
                
        return http.build();
    }
    // ...
}

 


 

2. 역할 기반 접근 제어(RBAC) 핵심 설정 분석

filterChain 메서드 내에서 < authorizeHttpRequests >를 통해 URL 패턴별 접근 권한을 설정한다. Spring Security는 설정된 순서대로 규칙을 적용하므로, 가장 구체적인 규칙을 위에, 가장 일반적인 규칙을 아래에 배치하는 것이 중요하다.

 

2.1. 비회원 접근 허용 (permitAll())

// 1. 비회원 (모두 접근 가능)
.requestMatchers("/", "/sign_up", "/signup_save",
        "/check/**", "/image/**", "/css/**", "/js/**").permitAll()
  • requestMatchers(...): 특정 URL 패턴을 지정한다. 여기서는 메인 페이지(/), 회원가입 관련 페이지(/sign_up, /signup_save), 리소스 파일(이미지, CSS, JS) 경로 등을 포함한다.
  • .permitAll(): 이 패턴에 해당하는 요청은 인증 여부(로그인 여부)와 관계없이 모든 사용자에게 접근을 허용한다. 즉, 비회원도 접근할 수 있게 된다.

 

2.2. 관리자 전용 접근 (hasRole("ADMIN"))

// 2. 관리자 (ADMIN 역할만 접근 가능
.requestMatchers("/admin/**").hasRole("ADMIN")
  • requestMatchers("/admin/**"): /admin으로 시작하는 모든 경로를 지정한다. (예: /admin/users, /admin/dashboard).
  • .hasRole("ADMIN"): 해당 요청은 ADMIN 역할을 가진 인증된 사용자만 접근할 수 있다. Spring Security는 내부적으로 ROLE_ 접두사를 자동으로 붙여 ROLE_ADMIN 권한을 확인한다.

 

2.3. 일반 회원 전용 접근 (hasRole("MEMBER"))

// 3. 일반 회원(MEMBER 역할만 접근 가능)
.requestMatchers("/member/**").hasRole("MEMBER")
  • requestMatchers("/member/**"): /member로 시작하는 모든 경로를 지정한다.
  • .hasRole("MEMBER"): 해당 요청은 MEMBER 역할을 가진 인증된 사용자만 접근할 수 있게 된다. (내부적으로 ROLE_MEMBER 권한 확인).

 

2.4. 로그인 필수 (인증된 사용자) (authenticated())

// 4. 나머지 모든 요청은 로그인만 하면 접근 가능(인증)
// ( 로그인 = ADMIN, MEMBER 모두 해당된다 )
.anyRequest().authenticated()
  • .anyRequest(): 위에 명시된 규칙을 제외한 나머지 모든 요청을 지정한다.
  • .authenticated(): 해당 요청은 인증된 사용자, 즉 로그인한 사용자만 접근할 수 있게 되는 것이다. 관리자(ADMIN)든 일반 회원(MEMBER)이든 로그인만 했다면 접근 가능

 

3. 예외 처리: 접근 거부 시 동작 설정

사용자가 로그인했으나 접근 권한이 없는 페이지를 요청했을 때(예: 일반 회원이 /admin 페이지에 접근 시도)의 동작을 설정한다.

.exceptionHandling(exception -> exception
        .accessDeniedPage("/login")
)
  • exceptionHandling: Spring Security의 예외 처리 방법을 설정한다.
  • accessDeniedPage("/login"): 권한이 없어 접근이 거부될 경우(HTTP Status 403 Forbidden) 사용자를 /login 페이지로 리디렉션한다.

 

4. 인증 설정: 폼 로그인, 로그아웃, 비밀번호 암호화

4.1. 폼 기반 로그인 설정

.formLogin(form -> form
        .loginPage("/login")
        .loginProcessingUrl("/go_login")
        .usernameParameter("id")
        .passwordParameter("pw")
        .defaultSuccessUrl("/")
        .permitAll()
)
  • .loginPage("/login"): 커스텀 로그인 페이지 URL을 지정합니다.
  • .loginProcessingUrl("/go_login"): 로그인 처리를 담당할 URL을 지정합니다. (실제 컨트롤러 구현은 필요 없음, Spring Security가 처리).
  • .usernameParameter("id"), .passwordParameter("pw"): 로그인 폼에서 사용할 사용자 ID와 비밀번호 파라미터 이름을 지정합니다.
  • .defaultSuccessUrl("/"): 로그인 성공 시 기본으로 이동할 페이지입니다.

 

4.2. 로그아웃 설정

.logout(logout -> logout
        .logoutUrl("/logout")
        .logoutSuccessUrl("/login")
        .permitAll()
)
  • .logoutUrl("/logout"): 로그아웃을 처리할 URL을 지정합니다.
  • .logoutSuccessUrl("/login"): 로그아웃 성공 시 이동할 페이지입니다.

 

4.3. 비밀번호 암호화 (PasswordEncoder)

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
  • BCryptPasswordEncoder: Spring Security에서 권장하는 비밀번호 암호화 방식 중 하나입니다. 실제 DB에 저장된 사용자의 비밀번호는 이 인코더를 통해 암호화되어 있어야 합니다.

 


 

🚫 문제 발견

3. 예외처리: 접근 거부 시 동작 설정에서 관리자 외에는 로그인 페이지로 이동하라고 설정해뒀다. 이는 페이지 이동이 매우 불편해지니 관리자만 사용할 수 있는 버튼이 있게 top.html(네비게이션 바)를 간단하게 설정해보자.

<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <ul class="nav navbar-nav">
            <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">TEST02 <span
                    class="caret"></span></a>
                <ul class="dropdown-menu">
                    // 해당 부분 추가
                    <li sec:authorize="hasRole('ADMIN')">
                        <a href="/admin/input02">test02 입력</a>
                    </li>
                    
                    <li><a href="output02">test02 출력</a></li>
                </ul>
            </li>
        </ul>
        </div>
</nav>

작동 원리

  1. sec:authorize="hasRole('ADMIN')": Thymeleaf가 HTML을 렌더링할 때, 현재 로그인된 사용자에게 ROLE_ADMIN 권한이 있는지 확인한다.
  2. 관리자인 경우: 조건이 참(true)이므로, 해당 <li> 태그와 그 안에 있는 <a href="/admin/input02">test02 입력</a>가 HTML 페이지에 정상적으로 포함되어 보이게 된다.
  3. 관리자가 아닌 경우 (일반 회원 또는 비회원): 조건이 거짓(false)이므로, 해당 <li> 태그 전체가 HTML 소스코드에서 완전히 제거되어 사용자에게는 보이지 않게 된다.

 

참고

일반 회원이 주소창에 /admin/input02를 직접 입력하고 접근을 시도하면 여전히 Spring Security의 인가(Authorization) 처리가 필요하다.

 

따라서, 버튼 숨기기와 이전에 설정하신 < SecurityConfig의 hasRole("ADMIN") >을 함께 사용하는 것이 가장 안전한 방법이다.

// SecurityConfig.java
.authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN") // ◀ 서버에서 ADMIN 권한만 허용
    // ...
)
.exceptionHandling(exception -> exception
    .accessDeniedPage("/admin_page") // ◀ 권한 부족 시 전용 페이지로 리다이렉트
)

 

 /login → /admin_page로 변경 후에 admin_page.html을 새로 만들었다.

해당 페이지는 관리자만 이용가능하다라는 문자열을 <p>로만 넣어뒀기에 일반 회원이 /admin/input02를 직접 입력하고 접속 시도하면 [ 관리자만 이용가능한 페이지입니다. ] 라는 메시지가 떠서 보안할 수 있게 된다.

 

 


 

 

이러한 설정을 통해, 여러분의 Spring Boot 애플리케이션은 사용자 역할에 따라 안전하게 리소스를 보호하는 강력한 역할 기반 접근 제어(RBAC) 시스템을 갖추게 됩니다. 이제 지정된 역할(ADMIN, MEMBER)을 사용자에게 부여하는 로직(예: UserDetailsService 구현체)만 잘 구현하면 완벽한 보안 시스템이 구축된다고 한다!

반응형