본문 바로가기
Next.js

React 애플리케이션의 고급 권한 관리 시스템 구현하기: 실제 코드와 주석으로 이해하기

by code2772 2024. 11. 13.

[ 목차 ]

    728x90
    반응형

    0. React 애플리케이션의 권한 관리 시스템 흐름도

    • 초기 접근 프로세스
      • 사용자 접근
      • 토큰 확인
      • 토큰 유효성 검사
      • 권한 검증
    • 기능별 권한 검증 프로세스
      • CRUD 작업별 권한 확인
      • 권한에 따른 UI 요소 표시/숨김
      • 기능 실행 전 권한 재확인
    • 에러 처리 프로세스
      • 권한 없음 처리
      • 토큰 만료 처리
      • 리프레시 실패 처리

    1. 소개

    안녕하세요! 오늘은 프로덕션 레벨의 React 애플리케이션에서 권한 관리 시스템을 어떻게 구현하는지, 상세한 코드와 주석을 통해 알아보겠습니다.

    2. 권한 관리의 핵심 구조

    2.1 withAuth HOC (Higher-Order Component)

    /**
     * 권한 검증을 위한 Higher-Order Component
     *@paramgssp - getServerSideProps 함수
     *@paramrequiredPermission - 필요한 권한 문자열
     *@returns GetServerSideProps 함수
     */
    const withAuth = (
        gssp: (context: GetServerSidePropsContext, store: Store) => Promise<{ props: any }>,
        requiredPermission?: string
    ): GetServerSideProps => {
        return wrapper.getServerSideProps((store) => async (context: GetServerSidePropsContext) => {
    // 요청에서 토큰 추출
            const { req, res, resolvedUrl } = context;
            const accessToken = req.cookies['accessToken'];
            const refreshToken = req.cookies['refreshToken'];
    
    // 메인 페이지이고 토큰이 없는 경우 바로 통과
            if (resolvedUrl === '/' &&  !accessToken && !refreshToken) {
                return await gssp(context, store);
            }
    
    // 권한 검증 로직
            if (requiredPermission) {
                const state = store.getState();
                const userPermissions = state.login.loginPermission?.permissions || [];
    
    // 필요한 권한이 없는 경우 403 에러 반환
                if (!userPermissions.includes(requiredPermission)) {
                    context.res.statusCode = 403;
                    return {
                        props: {
                            statusCode: 403,
                            message: "접근 권한이 없습니다."
                        }
                    };
                }
            }
    
    // 나머지 로직 처리...
        });
    }
    
    

    2.2 토큰 디코딩 기능

    /**
     * JWT 토큰을 디코딩하여 권한 정보 추출
     *@paramtoken - JWT 토큰 문자열
     *@returns TokenData 객체 (id, roles, permissions)
     */
    export function decodeTokenInfo(token: string): TokenData {
        try {
    // 토큰을 세 부분으로 분리
            const [header, payload, signature] = token.split('.');
    
    // Base64Url 디코딩
            const decodedPayload = base64UrlDecode(payload);
    
    // DEFLATE 압축 해제
            const inflated = pako.inflate(
                new Uint8Array(decodedPayload.split('').map(c => c.charCodeAt(0))),
                { to: 'string' }
            );
    
    // JSON 파싱
            const decodedToken = JSON.parse(inflated);
    
    // 권한 정보 추출
            const allClaims = decodedToken.roles || [];
            return {
                id: decodedToken.sub,
    // ROLE_ 로 시작하는 권한만 필터링
                roles: allClaims.filter(claim => claim.startsWith('ROLE_')),
    // PERM_ 로 시작하는 권한에서 접두사 제거
                permissions: allClaims.filter(claim => claim.startsWith('PERM_'))
                                    .map(perm => perm.replace('PERM_', ''))
            };
        } catch (error) {
            console.error('Token decoding failed:', error);
            return { id: '', roles: [], permissions: [] };
        }
    }
    
    

    3. 실제 구현 사례: 스팸 관리 시스템

    3.1 메인 컴포넌트 구현

    /**
     * 스팸 관리 시스템 메인 컴포넌트
     * 권한에 따른 기능 제어 구현
     */
    const Spam = () => {
    // Redux store에서 권한 정보 가져오기
        const permissions = useTypeSelector((state) => state.login.loginPermission.permissions);
    
    /**
         * 스팸 추가 기능 핸들러
         * CREATE 권한 확인 후 처리
         */
        const handleAddClick = () => {
            if(!permissions.includes(SPAM_MANAGEMENT_CREATE)){
                router.push('/spam/spam');
                return;
            }
            setIsAddModalOpen(true);
        };
    
    /**
         * 스팸 수정 기능 핸들러
         * UPDATE 권한 확인 후 처리
         */
        const handleEditClick = (row) => {
            if(!permissions.includes(SPAM_MANAGEMENT_UPDATE)){
                router.push('/spam/spam');
                return;
            }
            setEditingSpam(row);
            setNewValue('');
            setIsEditModalOpen(true);
        };
    
    /**
         * 스팸 삭제 기능 핸들러
         * DELETE 권한 확인 후 처리
         */
        const handleDelete = () => {
            if(!permissions.includes(SPAM_MANAGEMENT_DELETE)){
                router.push('/spam/spam');
                return;
            }
    
            if (selectedRows.length === 0) {
                openAlert('알림', '삭제할 항목을 선택해주세요.');
                return;
            }
    
            openConfirm('확인', '선택한 항목을 삭제하시겠습니까?', () => {
    // 선택된 항목 삭제 처리
                selectedRows.forEach(row => {
                    const content = row.type === 'GT' ? row.text :
                                  (row.type === 'CT' ? row.text : row.text);
                    dispatch(deleteSpamRequest({
                        spamType: 'spam',
                        id: row.id,
                        cpid: row.id,
                        type: row.type,
                        content
                    }));
                });
            });
        };
    }
    
    

    3.2 테이블 컬럼 설정

    /**
     * 동적 테이블 컬럼 설정
     * 권한에 따른 컬럼 표시 제어
     */
    const columns = [
        {
            header: '스팸',
            accessor: 'spamCount',
            width: '17%',
    // 스팸 타입에 따른 텍스트 렌더링
            render: (value: string, row: any) => {
                switch(row.type) {
                    case 'GT': return row.text;// 전체 스팸
                    case 'CT': return row.text;// 카테고리 스팸
                    default: return value;
                }
            }
        },
        {
            header: '카테고리',
            accessor: 'id',
            width: '13%',
    // 카테고리 ID 렌더링 로직
            render: (value: string) => value === ' ' ? '(ALL)' : value
        },
    // ... 기타 컬럼
        {
            header: '변경',
            accessor: 'edit',
            width: '12%',
    // 수정 권한이 있는 경우에만 버튼 표시
            render: (value: string, row: any) =>
                permissions.includes(SPAM_MANAGEMENT_UPDATE) ?
                    <EditButton row={row} onEditClick={handleEditClick} /> :
                    null
        }
    ];
    
    

    3.3 API 통신 및 에러 처리

    /**
     * 스팸 추가 API 호출 함수
     *@paramspamData - 추가할 스팸 데이터
     */
    const handleAddSpam = async (spamData) => {
        try {
    // API 호출
            const response = await api.post('/spam/spam/add', null, {
                params: {
                    type: spamData.type,
                    cpid: spamData.cpid,
                    content: spamData.content
                },
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('token')}`
                }
            });
    
    // 성공 시 처리
            if (response.status === 200) {
                dispatch(fetchSpamsRequest({
                    spamType: 'spam',
                    params: {types:['GT','CT'], page: 0, size: 40}
                }));
                handleCloseAddModal();
                openAlert('성공', '스팸이 성공적으로 추가되었습니다.');
            }
        } catch (error) {
    // 에러 처리
            console.error('스팸 추가 중 오류 발생:', error);
            let errorMessage = '스팸 추가 중 오류가 발생했습니다.';
    
    // 중복 데이터 에러 처리
            if (error.response?.data?.includes('ORA-00001')) {
                errorMessage = '이미 존재하는 스팸 데이터입니다.';
            }
    
            setAddError(errorMessage);
            openAlert('오류', errorMessage);
        }
    };
    
    

    4. 보안 관련 구현

    4.1 토큰 관리

    /**
     * 토큰 저장 및 보안 설정
     * HttpOnly, Secure, SameSite 설정으로 보안 강화
     */
    res.setHeader('Set-Cookie', [
        `accessToken=${token}; HttpOnly; Path=/; Secure; SameSite=Strict`,
        `refreshToken=${refreshToken}; HttpOnly; Path=/; Secure; SameSite=Strict`
    ]);
    
    /**
     * 토큰 만료 시 리프레시 처리
     */
    if (accessToken && refreshToken) {
        try {
    // 리프레시 토큰으로 새 액세스 토큰 발급
            store.dispatch(refreshTokenRequest());
    // 사용자 정보 다시 로드
            store.dispatch(loadMyInfoRequest());
    // ... 추가 처리
        } catch (error) {
            console.error('토큰 리프레시 실패:', error);
        }
    }
    
    

    5. 결론

    이러한 방식의 권한 관리 시스템은 다음과 같은 장점을 제공합니다:

    1. 체계적인 권한 검증: HOC를 통한 일관된 권한 검증
    2. 세분화된 접근 제어: 기능별 세밀한 권한 관리
    3. 보안성: HttpOnly 쿠키, 토큰 리프레시 등 보안 기능 구현
    4. 사용자 경험: 적절한 에러 처리와 피드백
    5. 유지보수성: 모듈화된 구조로 유지보수 용이

    이러한 구조는 특히 엔터프라이즈 환경에서 요구되는 복잡한 권한 관리를 효과적으로 구현할 수 있게 해줍니다. 각 코드 블록에 대한 자세한 주석을 통해 시스템의 작동 방식을 더 쉽게 이해하고 수정할 수 있습니다.

    반응형