bookshelf/INSTRUCTIONS.md at exercises/04-authentication · kentcdodds/bookshelf (github.com)
Background
HTTP requests 인증
특정 사용자에게 서비스를 제공하기 위해선 사용자 식별이 필요하며,
해당 사용자의 정보의 보안을 유지할 필요가 있다.
보통 id/pw 방법을 많이 사용한다.
특정 사용자를 식별하고, 권한을 확인한 뒤, 서비스를 제공하기 위한 수단이 필요하다.
그러기 위해선 매 요청마다 어떤 정보를 같이 보내야 한다.
매 요청마다 id/pw를 요구하거나, 서버에 해당 정보를 같이 보낼 수는 없다.
가장 간단한 방법은 토큰을 사용하는 것이다. 무효한 토큰이면 재인증을 요구하여 갱신한다.
토큰을 첨부하는 일반적인 방법은 "Authorization"이라는 특수 요청 헤더를 사용하는 것이다.
인증된 요청을 만드는 방법
window.fetch('http://example.com/pets', {
headers: {
Authorization: `Bearer ${token}`,
},
})
jwt 더 알아보기 : https://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)
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)
/** @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}