서버가 session id를 기록하는 법
- 세션id를 어떻게 알아 챘느냐? > 쿠키로 인해서..!
쿠키란
서버에서 보내주는 작은 데이터 조각으로, 서버에서 셋팅해주는 값
서버에서 리스폰스 헤더에 담아서 전달해주면, 브라우저는 들고 있다가 같은 도메인으로 요청할 때 쿠키를 자동으로 담아서 전송한다.
서버가 브라우저에 쿠키를 저장하는 방법
- 문제점: 설정 등이 잘못되었을 경우 에러를 안내고 조용히 없어지기 때문에 찾는데 시간이 걸릴 수 있다.
브라우저가 쿠키를 서버에 전송하는 방식
쿠키는 클라이언트에서 건들면 안되는 것
쿠키 관련 정책 지정하기
- SameSite: None, Lax, Strict
- 이 쿠키가 어떤 도메인에서 움직이게 할것이냐
- 보안: None < Lax < Strict
- none은 아무 도메인에서나 가져가서 쓸 수 있음. (getCookie로 가져올 수 있음)
- 크롬에선 none을 못쓰게 막음
- Lax는 안전한 도메인 몇개에 관해서만 CrossSite 허용 (https, http only 속성 등의 조건이 갖춰졌을 경우)
- Strict는 정확하게 같은 Origin만 허용
- Http Only
- Http Only란? 통신전용으로, 클라이언트에서 가져가지 말라고 막아논 것..XSS같은 공격으로 쿠키를 털릴 일도 없음, js실행 안됨..
- Secure
- 패킷을 암호화해서 보냄
1. 로그인 호출 & 라우터 등록
- 라우터에 로그인 페이지 등록 및 관련 유틸 구현 (router.tsx)
- 로그인 페이지 내 로그인 로직 구현 (pages/Login.tsx)
- 로그인 함수 구현 (api/login.ts)
- body에 username과 password를 담아서 전송
- 로그인 성공 시 세션에 유저 정보 저장
- 성공 시 세션이 생성되며, 이후 별도 인증 없이 접근 가능한 /profile을 통해 유저 정보를 가져올 수 있다.
1) 라우터 객체 배열에 로그인 페이지 추가
interface RouterElement {
id: number // 페이지 아이디 (반복문용 고유값)
path: string // 페이지 경로
label: string // 사이드바에 표시할 페이지 이름
element: React.ReactNode // 페이지 엘리먼트
withAuth?: boolean // 인증이 필요한 페이지 여부
// icon: React.ReactNode // 사이드바에 표시할 아이콘
}
const routerData: RouterElement[] = [
// 로그인 불필요 페이지들 라우터 등록하기
{
id: 0,
path: '/',
label: 'Home',
element: <Home/>,
withAuth: false
},
{
id: 1,
path: '/login',
label: '로그인',
element: <Login/>,
withAuth: false
},
// 로그인 필요 페이지 a, b, c 등록하기
{
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
}
]
2) 라우터 프로바이더에 필요한 형태로 전달
export const routers: RemixRouter = createBrowserRouter(
// 인증이 필요한 페이지는 GeneralLayout으로 감싸기
// GeneralLayout 에는 페이지 컴포넌트를 children 으로 전달
routerData.map((router) => {
if (router.withAuth) {
return {
path: router.path,
element: <GeneralLayout>{ router.element }</GeneralLayout>
}
} else {
return {
path: router.path,
element: router.element
}
}
})
)
function App() {
return (
<RouterProvider router={routers} />
)
}
3) 로그인 API 호출
const fetchClient = async (url: string, options: RequestInit) => {
return fetch(url, {
headers: {
'Content-Type': 'application/json',
credentials: 'include',
},
...options
})
}
export const login = async (args: LoginRequest): Promise<LoginResult> => {
// POST, '/auth/login' 호출
const loginRes = await fetchClient(`${BASE_URL}/auth/login`, {
method: 'POST',
body: JSON.stringify(args)
})
return loginRes.ok ? 'success' : 'fail'
}
4) 로그인 API 호출 결과에 따라 페이지 제어
const Login = () => {
const { routeTo } = useRouter()
const loginSubmitHandler = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// FormData를 이용해서 로그인 시도
const formData = new FormData(event.currentTarget)
const loginResult = await login({
username: formData.get('username') as string,
password: formData.get('password') as string
})
// 로그인 실패시 함수 종료. 로그인 성공시 '/page-a'로 이동
if (loginResult === 'fail') {
alert('로그인 실패')
return
}
routeTo('/page-a')
}
// 이미 로그인된 상태라면 page-a로 라우팅
const checkLoginStatus = useCallback(async () => {
const isUserLoggedIn: boolean = await isLoggedIn()
if (isUserLoggedIn) {
routeTo('/page-a')
return
}
}, [routeTo])
useEffect(() => {
checkLoginStatus()
}, [checkLoginStatus])
return (<div className="non-logged-in-body">
<h1>
로그인 페이지
</h1>
<form onSubmit={loginSubmitHandler}>
...
<button type="submit" value="Submit">submit</button>
</form>
</div>)
}
2. 로그인 상태기반 페이지 분기 & 확인
- 유저 정보 가져오기 (GET /profile)
- 세션에 저장된 유저 정보를 반환
- credentials: 'include'옵션을 활성화 하는 경우 별도 개발 없이도 자동으로 로그인 여부를 검증하므로, 유저 정보 수신 성공여부만 확인하면 됨
1.사이드바 생성
2.페이지를 이동할 때 마다 로그인 여부를 확인하고 로그인되지 않은 경우 ‘/login’ 페이지로 이동
3.페이지를 이동할 때 마다 유저 정보를 가져와 sidebar에 표시
1) 사이드바 생성
export const SidebarContent: SidebarElement[] = routerData.reduce((prev, router) => {
// 인증이 필요한 페이지만 사이드바에 표시하기
if (!router.withAuth) return prev
return [
...prev,
{
id: router.id,
path: router.path,
label: router.label
}
]
}, [] as SidebarElement[])
return (<div className="general-layout">
<Sidebar sidebarContent={SidebarContent} userProfile={userProfile } />
<div className="general-layout__body">
{ children }
</div>
</div>)
2) 로그인 여부 확인하기 - GET '/profile'호출
export const getCurrentUserInfo = async (): Promise<User | null> => {
// 호출 성공 시 유저 정보 반환
const userInfoRes = await fetch(`${ BASE_URL }/profile`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
credentilas: 'include
}
})
return userInfoRes ? userInfoRes.json() : null
}
}
3) 페이지 이동시마다 로그인 여부 체크하기
페이지 이동 시 마다 로그인 여부 확인
const fetchUserProfile = useCallback(async () => {
// 페이지 이동시 마다 로그인 여부를 확인하는 함수
const userProfileResponse = await getCurrentUserInfo()
if (userProfileResponse === null) {
routeTo('/login')
return
}
setUserProfile(userProfileResponse)
}, [])
useEffect(() => {
// 페이지 이동시 마다 로그인 여부를 확인하는 함수 실행
console.log('page changed!')
fetchUserProfile()
}, [children])
- useCallback 사용한 이유: callback이 페이지가 업데이트 될 때마다 새로 만들어졌었어야해서
- async를 useEffect에서 직접 호출하는 것 보다, callback으로 만들어 놓고 안에 있는 async가 호출되는 함수들이 업데이트됐을 때 callback을 새로 만들어서 useEffect에 넣어주면 좋기 때문
- Depth Array는 참고 x
3. 만들어진 어플리케이션 전체 구조 구경하기
4. 세션 로그인 리뷰
다시 보는 세션 로그인 개념
- sessionId는 쿠키에 넣어서 왔다갔다 했다.
- 별도 설정하지 않아도, credential: true설정으로 알아서 왔다갔다함
권한이 필요한 페이지 접근 시마다 API를 요청하는게 일반적인가?
- 어차피 권한이 없으면 API 요청이 실패함
- API요청 후에 실패하면 로그인페이지로 돌아가도 무방
지난번 도메인 호출과 인프라 구성 비교하기
- 토큰구조의 내부적 인프라 구성
- wanted-p2 bluestraggl.rcom 도메인으로 전송
- 그 안에는 DNS구성(Route53),로드밸런서(ALB),EC2(인스턴스)로 구성
-세션방식:서버, 클라이언트 동시에 띄움 (같은 pc에서 왔다갔다 하기 때문에 CORS문제 해결 가능)
5. 세션 로그인 동작 살펴보기
- 토큰기반과 다르게 유저진입 시 세션이 유효한지 체크(서버에 물어봄)
- (토큰기반은 토큰이 유효한지 체크, 토큰이 맞는지는 서버에 보내서 확인)
- 토큰 재발급, 리프레쉬 없이 로그인 진행 시 sessionId를 받는다.(검증절차 간단)
🔗 프리온보딩 학습내용입니다.