Next.js

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

code2772 2024. 11. 13. 08:19
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. 유지보수성: 모듈화된 구조로 유지보수 용이

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

반응형