원티드 프리온보딩 - 로그인 기능 구현하기 4 (실습)
원티드 프리온보딩 - 로그인 기능 구현하기 4강 실습
유저 권한에 따라 접근이 제어되는 웹 만들기
1. 어드민 페이지 추가하기
isAdminPage: 어드민 페이지 여부
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const routerData = [
{
id: 0,
path: "/",
label: "Home",
element: <Home />,
withAuth: false
},
{
id: 1,
path: "/login",
label: "로그인",
element: <Login />,
withAuth: false
},
{
id: 2,
path: "/page-a",
label: "페이지 A",
element: <PageA />,
withAuth: true
},
{
id: 3,
path: "/page-b",
label: "페이지 B",
element: <PageB />,
withAuth: true
},
{
id: 4,
path: "/page-c",
label: "페이지 C",
element: <PageC />,
withAuth: true
},
{
id: 5,
path: "/admin",
label: "어드민 페이지",
element: <AdminPage />,
withAuth: true,
isAdminPage: true
}
];
2. 어드민 전용 페이지는 isAdminPage
의 값 전달시키기
isAdminPage={router.isAdminPage}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 어드민 전용 페이지는 isAdminPage = true를 전달
export const routers = createBrowserRouter(
routerData.map((router) => {
if (router.withAuth) {
return {
path: router.path,
element: (
<GeneralLayout isAdminPage={router.isAdminPage}>
{router.element}
</GeneralLayout>
)
};
} else {
return {
path: router.path,
element: router.element
};
}
})
);
3. 사이드바(메뉴)에 admin 페이지로 이동하는 요소는 admin에게만 보여주도록 하기
isAdminOnly
를 추가해 선택적으로 보여지도록 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
export const SidebarContent = routerData.reduce((prev, router) => {
if (!router.withAuth) return prev;
return [
...prev,
{
id: router.id,
path: router.path,
label: router.label,
isAdminOnly: router.isAdminPage
}
];
}, []);
4. 사이드바 컴포넌트에서 필터를 통해 isAdminOnly가 true이고, userInfo의 role에 ‘admin’이 포함된 경우에만 admin으로 이동할 수 있는 요소 렌더링하기
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const Sidebar = ({ sidebarContent, userProfile }) => {
const { currentPath, routeTo } = useRouter();
const sidebarMenuClickHandler = (path) => {
routeTo(path);
};
return (
<div className="sidebar">
<h3 className="sidebar-title">실습 3</h3>
<ul>
{sidebarContent
.filter((element) => {
return element.isAdminOnly
? userProfile?.userInfo.roles.includes("admin")
: !!userProfile;
})
.map((element) => {
return (
<li
key={element.path}
className={
currentPath === element.path
? "sidebar-menu selected"
: "sidebar-menu"
}
onClick={() => sidebarMenuClickHandler(element.path)}
>
{element.label}
</li>
);
})}
</ul>
<div>
{userProfile ? (
<div className="sidebar-footer">
{userProfile?.userInfo?.name}
{userProfile?.userInfo?.roles.includes("admin") ? "(admin)" : ""}님 환영합니다.
<button className={"small"} onClick={logoutHandler}>
로그아웃
</button>
</div>
) : (
<div>로그인이 필요합니다.</div>
)}
</div>
</div>
);
};
export default Sidebar;
■ 응답받은 userInfo의 roles에 따라 페이지 이동시키기 (권한이 없는 유저가 페이지로 이동 못하도록 막기)
routTo()
로 해당 페이지로 이동 & return <></>
아무것도 담겨있지 않은 것을 반환
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
32
33
34
35
36
37
38
39
40
41
42
43
const GeneralLayout = ({ children, isAdminPage }) => {
const [userProfile, setUserProfile] = useState(null);
const { routeTo } = useRouter();
/* 유저 정보 가져오기*/
const fetchUserProfile = useCallback(async () => {
const userProfileResponse = await getCurrentUserInfo();
if (userProfileResponse === null) {
routeTo("/login");
return;
}
setUserProfile(userProfileResponse);
}, []);
useEffect(() => {
fetchUserProfile();
}, [children]);
// 응답으로 받은 user의 userInfo.roles가 비어있다면 아무 권한이 없는 user이므로 로그인 페이지로 이동
if (userProfile?.userInfo.roles.length === 0) {
routeTo("/login");
return <></>;
}
// Admin 전용 페이지 접근 시도시 userProfile.userInfo.roles에 admin이 없는 경우에는 page-a로 이동
if (isAdminPage && !userProfile?.userInfo.roles.includes("admin")) {
routeTo("/page-a");
return <></>;
}
if (userProfile === null) return <div>loading...</div>;
return (
<div className="general-layout">
<Sidebar sidebarContent={SidebarContent} userProfile={userProfile} />
<div className="general-layout__body">{children}</div>
</div>
);
};
export default GeneralLayout;
■ 권한별 자원 조회하기
1. 아이템 가져오는 함수 작성
서버가 하는 일
- 아이템 가져오는 함수: DB에 있는 아이템들 중에 소유자를 확인하고 데이터 클라이언트에게 보내기
- 모든 아이템 가져오는 함수: 요청한 유저의 roles에 admin이 포함되어있는지 여부를 확인하고 데이터 클라이언트에게 보내기
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
// api/login.js
// 아이템을 가져오는 함수
export const getItems = async () => {
const itemsRes = await fetch(`${BASE_URL}/items`, {
method: "GET",
headers: {
"Content-Type": "application/json",
credentials: "include"
}
});
return itemsRes.ok ? itemsRes.json() : null;
};
// 모든 아이템을 가져오는 함수
export const getAllItems = async () => {
const allItemsRes = await fetch(`${BASE_URL}/all-items`, {
method: "GET",
headers: {
"Content-Type": "application/json",
credentials: "include"
}
});
return allItemsRes.ok ? allItemsRes.json() : null;
};
2. adminPage & page-a에서 아이템 가져오기
isUserItemsFetched
처음 렌더될때만 가져오도록 설정
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
// pages/AdminPage.jsx
const AdminPage = () => {
const [items, setItems] = useState(null);
const isUserItemsFetched = useRef(false);
const fetchUserItems = useCallback(async () => {
const userItems = await getAllItems();
if (!userItems || userItems !== null) setItems(userItems);
isUserItemsFetched.current = true;
}, []);
useEffect(() => {
if (!isUserItemsFetched.current) fetchUserItems();
}, []);
return (
<div>
<h1>AdminPage</h1>
<p>
roles 배열 안에 'admin'을 가진 유저에게만 접근을 허용합니다. 또한, 모든
아이템 목록을 가져옵니다. (어드민 전용 API)
</p>
<ItemList items={items} />
</div>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pages/PageA.jsx
const PageA = () => {
const [items, setItems] = useState(null);
const isUserItemsFetched = useRef(false);
const fetchUserItems = useCallback(async () => {
const userItems = await getItems();
if (!userItems || userItems !== null) setItems(userItems);
isUserItemsFetched.current = true;
}, []);
useEffect(() => {
if (!isUserItemsFetched.current) fetchUserItems();
}, []);
return (
<div>
<h1>Page A</h1>
<ItemList items={items} />
</div>
);
};
■ 로그아웃 기능 구현하기
세션기반 로그인일 경우 로그아웃을 할 때 쿠키에 저장되어 있는 값이 사라지지는 않고, 서버에서 해당 쿠키가 파괴되었다고 응답함
아무 의미가 없는 쿠키만 남아있음 (쿠키 유효기간이 만료되면 자동으로 삭제됨)
1
2
3
4
5
6
7
8
9
10
// api/login.js
export const logout = async () => {
await fetch(`${BASE_URL}/logout`, {
method: "POST",
headers: {
"Content-Type": "application/json",
credentials: "include"
}
});
};
This post is licensed under CC BY 4.0 by the author.