본문 바로가기
공부

React, Spring Security 기반 권한 관리(RBAC) 시스템 검토

by 꾸돼지 2025. 7. 6.
320x100

RBAC

RBAC란 Role Based Access Control을 말한다.

즉, 역할 기반으로 페이지나 기능의 접근을 제어하는 보안 및 권한에 관련된 기능이다.

MS 권한관리 예제

 

예를 들어, 사용자는 게시물을 보는 권한만을 갖고 있다.

관리자는 유저가 갖고 있는 권한에 더불어 게시물 등록과 사용자 관리 등의 기능도 사용할 수 있다.

 

이처럼 다양한 사용자는 각자의 역할에 따라 웹사이트의 기능을 사용할 권한을 부여받고 있다.

이 역할과 권한을 관리하는 것을 RBAC이라고 말한다.

 

위의 그림에서 만약 사용자에게 내부 게시판에 게시물을 등록할 권한을 주고 싶다면 사용자에게 admin 역할을 부여해야 하는 건가?

그러면 그 사용자는 게시물 등록뿐 아니라 사용자를 관리하는 권한마저 모두 갖게 될 것이다.

이런 경우, 사용자의 역할은 사용자로 유지하고, 임시적으로 게시물 등록이라는 권한만 부여하는 것이 올바른 접근일 것이다.

 

 

현재 상황

나는 Client Side Rendering인 React 환경에서 RBAC을 적용해야 한다.

JSP 등의 Server Side Rendering 환경에서는 모든 권한을 서버가 관리하고 있었기 때문에, RBAC의 적용이 매우 쉬웠다.

페이지를 요청한 사용자에게 화면을 보내주기 전, 권한을 먼저 확인하고 페이지를 보내주면 되었기 때문이다.

@Controller
...
@GetMapping("/admin")
public String admin(...) {
  if(!user.hasRole("role_admin") return "error";
  return "admin";
}

 

하지만 React 기반에서는 이런 세밀한 관리를 서버에서 수행하기 어렵다.

React는 사용자의 권한을 직접 확인하고, 그 권한에 맞는 화면이나 기능을 보여주거나 보여주지 않는 로직을 스스로 구현해야 한다.

...
{user.role === "role_admin" && <Link to={"/admin"}>admin page</Link> }
...
<button disabled={user.role !== "role_admin"}>생성</button>
...

 

FE 환경에서 권한별 기능관리는 위와 같은 숨김 처리나, 비활성화 처리를 많이 하는 편이다.

숨김은 사용자에게 권한이 없을 때, 화면에서 아예 인터페이스를 노출시키지 않는 방법이다.

비활성화는 위의 버튼처럼 권한이 없는 사용자는 사용할 수 없게 버튼을 잠금상태로 만드는 방법이다.

 

시스템의 규모가 작고, 롤에 대한 변경이 거의 없다면 위의 기능만으로도 충분할 것이다.

하지만 시스템의 규모가 크고, 사용자의 롤에 대한 정책이 자주 변경되는 대규모시스템이 있다면 위의 방식으로는 부족하다.

메뉴 기능이 수십 개인데, 사용자의 Role이 5개만 되어도 위의 로직에서 하나하나 분기해 주면 엄청난 길이의 파일이 될 것이다.

함수로 만들어서 처리할 수는 있지만, 이것 역시 프론트 소스가 비효율적으로 길어진다는 점에서는 마찬가지이다.

 

 

 

RBAC Architecture 원칙

React와 SpringBoot(SpringSecurity) 환경에서 보안성, 확장성, 유지보수성을 고려해야 한다.

 

핵심 아키텍처 원칙

  • 중앙집중형 권한 : 모든 권한 정보의 원천은 DB이다. BE/FE는 이 중앙화된 정보 기반으로 각자의 영역에서 정보를 통제한다.
  • 권한 기반 검증: 역할은 권한들의 묶음으로, 관리 편의성을 위한 수단이다.
  • 최소 권한 원칙: 모든 접근은 기본적으로 거부하고, 명시적으로 허용된 권한에 대해서만 접근이 가능하다.
  • 동적 / 실시간 반영: 관리자가 사용자 권한을 변경하면, 현재 활성화된 사용자도 실시간 반영한다. 
  • 감사 추적 기능: 사용자의 권한에 영향을 미치는 모든 변경 사항은 반드시 기록하고 추적 가능하게 만든다.

[동적 / 실시간 반영]

** 예전에 포스팅한 SSE 등을 이용하여 구현할 수 있다. 가장 쉬운 방법은 사용자가 한 번 로그아웃 후 재로그인하는 것이다. 

** 권한 정보는 잘 변하지 않으므로 캐시를 사용하기 좋은 데이터이다. 캐시를 사용하는 경우 캐시 무효 전략을 검토해야 한다. 

 

 

 

코어 아키텍처

데이터베이스 스키마

  • users
  • roles
  • permissions
  • user_roles
  • role_permissions
  • menu_items
  • api_endpoints

 

 

 

백엔드

 

아래와 같이 Security에서 메서드 단위로 보안을 적용하는 기능이 있다.

Security의 버전 별로 약간씩 다를 수 있다.

@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
  ...
}

 

위의 설정을 하면 아래처럼 메서드 단위로 권한을 설정할 수 있다.

@RestController
@RequestMapping("/api/admin")
public class AdminApiController {

    @Autowired
    private AdminService adminService;

    //@PreAuthorize("hasRole('role_admin')")
    @PreAuthorize("hasAuthority('GET_ADMIN_LIST')")
    @GetMapping("/admin")
    public String adminEndpoint() {
        return adminService.adminMethod();
    }
}

 

SpringSecurity는 아래와 같이 로그인할 때 사용자 정보와 권한 정보를 입력하는 부분이 기본적으로 존재한다.

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        
        List<GrantedAuthority> authorities = new ArrayList<>();
        // userEntity.getPermissions() -> ["GET_ADMIN_LIST", "CREATE_USER", ...]
        userEntity.getPermissions().forEach(permission -> {
            authorities.add(new SimpleGrantedAuthority(permission));
        });

        return new User(
                userEntity.getUsername(),
                userEntity.getPassword(),
                authorities);
    }
}

 

FE 소스가 꼬이거나 비정상적인 방법으로 권한이 없는 사용자가 요청을 보내면 403 Forbidden을 반환하게 된다.

 

 

 

 

프론트엔드

 

대부분 실무에서는 권한별 노출되는 페이지를 관리하기 위해 AdminRoute와 같은 합성컴포넌트를 만들어서 사용할 것이다.

import React from "react";
import { Navigate } from "react-router-dom";

type Role = "role_admin" | "role_user";

interface User {
  id: string;
  role: Role;
}

interface Props {
  children: React.ReactElement;
  user: User;
}

export default function AdminRoute({ children, user }: Props) {
  if (user.role !== "role_admin") {
    return <Navigate to="/" replace />;
  }
  return children;
}

 

비공개 화면에 들어가는 라우터 설정도 아래와 같이 할 것이다.

export default function App() {
  const user: User = { id: "123", role: "role_admin" };

  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        {user.role === "role_admin" && (
          <Link to="/admin"> | 관리자 페이지</Link>
        )}
      </nav>
      <Routes>
        <Route path="/" element={<div>Home</div>} />
        <Route
          path="/admin"
          element={
            <AdminRoute user={user}>
              <div>Admin</div>
            </AdminRoute>
          }
        />
        {/* 메뉴 목록을 서버에서 받은 값으로 반복문 처리 */}
      </Routes>
    </BrowserRouter>
  );
}

 

아마 버튼 등의 숨김을 위해서도 다음과 같은 합성 컴포넌트를 구성하게 될 것 같다.

<PermissionGuard requiredPermission="USER_DELETE">
  <button>사용자 삭제</button>
</PermissionGuard>

 

기능 내부에서 요청을 보내는 경우에도 한 번씩 권한 검사를 해보게 될 것이다.

이런 경우 커스텀 훅을 만드는 것도 생각해 볼 수 있다.

zustand 등에서 제공하는 내부 함수를 이용하는 것도 좋은 방법이다.

const { hasPermission } = usePermissions();

if (hasPermission('USER_EDIT')) {
  // 수정 로직 실행
}

 

 

BE-FE 권한 인터페이스

 

GET /api/auth/permissions 응답 예시

{
  "user": { "username": "testuser", "email": "test@example.com" },
  "roles": ["MANAGER", "EDITOR"],
  "permissions": [
    "USER_VIEW",
    "USER_CREATE",
    "POST_EDIT"
  ],
  "menu": [
    {
      "name": "사용자 관리",
      "path": "/users",
      "requiredPermission": "USER_VIEW",
      "children": [
        { "name": "사용자 생성", "path": "/users/create", "requiredPermission": "USER_CREATE" }
      ]
    }
  ]
}

 

위와 같이 사용자가 로그인한 이후, 권한에 대한 API Endpoint에서 해당 사용자의 권한정보를 받아올 수 있다.

역할과 권한, 메뉴 접근에 대한 내용은 모두 서버에서 관리하고, 프론트엔드 역시 BE를 통해 서버의 권한 정보를 받아온다.

 

 


기존 프로젝트에서는 이렇게 권한에 대해 생각해 본 적이 별로 없었다.

사용자 수도 적고, 고객이 별도의 포트에서 관리자 페이지가 실행되기를 바랐었다.

그래서 분기로 권한을 설정하는 것이 아니라 아예 프로젝트를 분리해서 만들었기 때문이다.

 

전 회사에서 전자정부 프레임워크 기반의 RBAC 시스템을 유지보수한 기억이 있었는데...

제대로 만들지도 못하고, 운영한 지 너무 오래된 시스템이라 종종 DB 내부에서 권한이 엉키는 경우가 많았었다.

그때 많이 참고했던 게 이 포스팅에서 소개한 권한 관리 컴포넌트이다.

 

 

[오픈소스] 사수 없이 막막했던 신입 시절, 나를 성장시킨 전자정부 표준프레임워크

전자정부 표준프레임워크를 처음 알게 된 건 2020년도 6월에 국비학원을 수료할 때쯤이었다.강사님이 7월에 무료 교육일정이 있고, 수업 내용을 잘 기억하고 있을 때 들어보라고 하셔서 신청해서

gooduck.net

 

 

이번 프로젝트에서 잘 검토하고 구현해서 경험을 쌓아야겠다.

많이 사용될 것 같은 부분이다.

320x100