본문 바로가기

프론트엔드

[로그인 구현] Session 방식 로그인

서버가 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. 로그인 호출 & 라우터 등록

  1. 라우터에 로그인 페이지 등록 및 관련 유틸 구현 (router.tsx)
  2. 로그인 페이지 내 로그인 로직 구현 (pages/Login.tsx)
  3. 로그인 함수 구현 (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를 받는다.(검증절차 간단)

🔗 프리온보딩 학습내용입니다.