본문 바로가기

카테고리 없음

[Epic React][Build an Epic React App][Authentication][인증]

반응형

bookshelf/INSTRUCTIONS.md at exercises/04-authentication · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

Background

HTTP requests 인증

특정 사용자에게 서비스를 제공하기 위해선 사용자 식별이 필요하며,

해당 사용자의 정보의 보안을 유지할 필요가 있다.

보통 id/pw 방법을 많이 사용한다.

특정 사용자를 식별하고, 권한을 확인한 뒤, 서비스를 제공하기 위한 수단이 필요하다.

그러기 위해선 매 요청마다 어떤 정보를 같이 보내야 한다.

매 요청마다 id/pw를 요구하거나, 서버에 해당 정보를 같이 보낼 수는 없다.

 

가장 간단한 방법은 토큰을 사용하는 것이다. 무효한 토큰이면 재인증을 요구하여 갱신한다.

토큰을 첨부하는 일반적인 방법은 "Authorization"이라는 특수 요청 헤더를 사용하는 것이다.

window.fetch 주위에 작은 래퍼가 있다면 모든 요청에 ​​이 토큰을 자동으로 포함할 수 있다.

인증된 요청을 만드는 방법

토큰은 실제로 사용자를 고유하게 식별하는 모든 것이 될 수 있지만 일반적인 표준은 JWT(JSON Web Token)를 사용하는 것이다. 📜
window.fetch('http://example.com/pets', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
})

jwt 더 알아보기 : https://jwt.io

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

직접 백엔드를 구현하는 방법 외에도 인증 서비스를 이용하는 방법이 있다.

Auth0,Netlify Identity,Firebase Authentication.

어떤 방식을 사용하던 구현은 아래와 같은 모양새가 된다.

const token = await authProvider.getToken()
const headers = {
  Authorization: token ? `Bearer ${token}` : undefined,
}
window.fetch('http://example.com/pets', {headers})

리액트의 인증 구현

상태 + 요청이므로 동일하게 useState + useEffect이다.

사용자가 사용자 이름과 비밀번호를 제공하면 인증 요청을 하고 요청이 성공하면 해당 토큰을 사용하여 추가 인증 요청을 할 수 있다.
(종종 서버는 토큰 외에 저장해서 사용할 만한 정보들을 제공한다.)
사용자가 이미 로그인 된 상황이라면 화면을 보여주고, 아니면 스피너를 보여준다.

0. 로그인 서비스 제공자 클라이언트 구현

auth-provider.js

이런식으로 구현된다는 것만 보자. 보통 다른 라이브러리를 사용한다.

bookshelf/auth-provider.js at exercises/04-authentication · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

1. 인증을 위한 client 구현

인증 관련 처리만 해주는 클라이언트를 구현한다.

import * as auth from 'auth-provider'
const apiURL = process.env.REACT_APP_API_URL

function client(
  endpoint,
  {data, token, headers: customHeaders, ...customConfig} = {},
) {
  // 데이터 전송 지원
  const config = {
    method: data ? 'POST' : 'GET',
    body: data ? JSON.stringify(data) : undefined,
    headers: {
      Authorization: token ? `Bearer ${token}` : undefined,
      'Content-Type': data ? 'application/json' : undefined,
      ...customHeaders,
    },
    ...customConfig,
  }

  // 401 Unauthorized시 로그아웃
  return window.fetch(`${apiURL}/${endpoint}`, config).then(async response => {
    if (response.status === 401) {
      await auth.logout()
      // refresh the page for them
      window.location.assign(window.location)
      return Promise.reject({message: 'Please re-authenticate.'})
    }
    const data = await response.json()
    if (response.ok) {
      return data
    } else {
      return Promise.reject(data)
    }
  })
}

export {client}

2. 사용법

useAsync는 아래에서 코드를 확인한다.

  • 토큰이 유효한지 확인한다.
  • 토큰 검사 중에 로딩 화면을 보여준다.
  • 로그인 여부에 따라 다른 화면을 보여준다.
    • 겹치는 부분이 많으면 팩토리 메서드 패턴을 이용한다.

bookshelf/hooks.js at exercises/04-authentication · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

/** @jsx jsx */
import {jsx} from '@emotion/core'

import * as React from 'react'
import * as auth from 'auth-provider'
import {FullPageSpinner} from './components/lib'
import * as colors from './styles/colors'
import {client} from './utils/api-client'
import {useAsync} from './utils/hooks'
import {AuthenticatedApp} from './authenticated-app'
import {UnauthenticatedApp} from './unauthenticated-app'

// 토큰 유효한지 확인.
async function getUser() {
  let user = null

  const token = await auth.getToken()
  if (token) {
    const data = await client('me', {token})
    user = data.user
  }

  return user
}


function App() {
  const {
    data: user,
    error,
    isLoading,
    isIdle,
    isError,
    isSuccess,
    run,
    setData,
  } = useAsync()

  React.useEffect(() => {
    run(getUser())
  }, [run])

  // setData로 async data 동기화.
  const login = form => auth.login(form).then(user => setData(user))
  // setData로 async data 동기화.
  const register = form => auth.register(form).then(user => setData(user))
  const logout = () => {
    auth.logout()
    setData(null)
  }
  // idle은 쿼리 요청 전. isLoading은 로딩 중. 마운트 돠면 일단 로그인 여부 쿼리는 반드시 한번 날림.
  if (isLoading || isIdle) {
    return <FullPageSpinner />
  }

  if (isError) {
    return (
      <div
        css={{
          color: colors.danger,
          height: '100vh',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <p>Uh oh... There's a problem. Try refreshing the app.</p>
        <pre>{error.message}</pre>
      </div>
    )
  }
  // 로그인 여부에 따라 다른 화면 보여줌
  if (isSuccess) {
    return user ? (
      <AuthenticatedApp user={user} logout={logout} />
    ) : (
      <UnauthenticatedApp login={login} register={register} />
    )
  }
}

export {App}
반응형