1. 로그인 기능 연결
로그인(POST)
URL: urlbody: { username: string, password: string }
response { access_token: string }**
type LoginResult = 'success' | 'fail'
export type LoginResultWithToken = {
result: 'success'
access_token: string
} | {
result: 'fail'
access_token: null
}
export interface LoginRequest {
username: string
password: string
}
export const loginWithToken = async (args: LoginRequest): Promise<LoginResultWithToken> => {
const loginRes = await fetch(`${ BASE_URL }/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(args)
})
if (loginRes.ok) {
const loginResponseData = await loginRes.json()
return {
result: 'success',
access_token: loginResponseData.access_token
}
}
return {
result: 'fail',
access_token: null
}
}
유저 정보(GET)
URL: urlHeader에 Authorzation: Bearer ${access_token}포함response { access_token: string }
export const getCurrentUserInfoWithToken = async (token: string): Promise<UserInfo | null> => {
const userInfoRes = await fetch(`${ BASE_URL }/profile`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ token }`
}
})
if (userInfoRes.ok) {
return userInfoRes.json() as Promise<UserInfo>
}
return null
}
로그인 요청하기
const JWTLogin = () => {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const loginSubmitHandler = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget)
const loginResult = await loginWithToken({
username: formData.get('username') as string,
password: formData.get('password') as string
})
if (loginResult.result === 'fail') return
}
return (
<div>
<h1>
Login with Mock API
</h1>
<form onSubmit={loginSubmitHandler}>
...
<button type="submit" value="Submit">submit</button>
</form>
</div>
)
}
로그인 요청 형태 확인
로그인 결과 토큰으로 유저 정보 요청하고 보여주기
const JWTLogin = () => {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const loginSubmitHandler = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget)
const loginResult = await loginWithToken({
username: formData.get('username') as string,
password: formData.get('password') as string
})
if (loginResult.result === 'fail') return
const userInfo = await getCurrentUserInfoWithToken(loginResult.access_token)
if (userInfo === null) return
setUserInfo(userInfo)
}
return (
<div>
<h1>
Login with Mock API
</h1>
<form onSubmit={loginSubmitHandler}>
...
<button type="submit" value="Submit">submit</button>
</form>
</div>
)
}
2. 자동 로그인 구현
토큰 반환 대신 로컬 스토리지에 저장하기
export const login = async (args: LoginRequest): Promise<LoginResult> => {
const loginRes = await fetch(`${ BASE_URL }/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(args)
})
if (loginRes.ok) {
const loginResponseData = await loginRes.json()
saveAccessTokenToLocalStorage(loginResponseData.access_token)
return 'success'
}
return 'fail'
}
export const saveAccessTokenToLocalStorage = (accessToken: string) => {
localStorage.setItem('accessToken', accessToken)
}
토큰을 직접 전달하지 않고 함수만 호출하도록 변경
const loginSubmitHandler = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget)
const loginResult = await login({
username: formData.get('username') as string,
password: formData.get('password') as string
})
if (loginResult.result === 'fail') return
const userInfo = await getCurrentUserInfo()
if (userInfo === null) return
setUserInfo(userInfo)
}
로컬 스토리지에 있는 값을 가져와서 로그인
export const getCurrentUserInfo = async (): Promise<UserInfo | null> => {
const userInfoRes = await fetch( `${BASE_URL}/profile`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ getAccessTokenFromLocalStorage() }`
}
})
if (userInfoRes.ok) {
return userInfoRes.json() as Promise<UserInfo>
}
return null
}
export const getAccessTokenFromLocalStorage = (): string => {
return localStorage.getItem('accessToken') || ''
}
다른 페이지도 로컬 스토리지에 저장되어 있는 토큰 사용
const getUserInfo = useCallback (async () => {
const userInfo = await getCurrentUserInfo()
if (userInfo === null) return
setUserInfo(userInfo)
isDataFetched.current = true
}, [])
useEffect(() => {
if (isDataFetched.current) return
getUserInfo()
}, [])
좀 더 개발하기 좋은 프로덕트로 만들기
export const getCurrentUserInfo = async (): Promise<UserInfo | null> => {
const userInfoRes = await fetchClient(
`${ BASE_URL }/profile`,
{ method: 'GET' }
)
if (userInfoRes.ok) {
return userInfoRes.json() as Promise<UserInfo>
}
return null
}
export const fetchClient = async (url: string, options: RequestInit): Promise<Response> => {
const accessToken = getAccessTokenFromLocalStorage()
const newOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${ accessToken }`
}
}
return fetch(url, newOptions)
}
3. Refresh Token
Axios instance에 기본 동작 추가하기
Axios 사용 이유: error에 따라서 response가 끝나기 전에 중간 조치(interceptor)할 때 사용하기 편함
- axiosInstance에 설정을 달아주면, 요청이 가거나 올 때 instance설정(ex:baseURL)을 붙여준다.
- interceptior를 달아주면 API를 주고받는 중간에 특정 상황에서 수행할 함수를 넣을 수 있다.
- onFulfilled: 성공
- onRejected: 실패
- 에러코드가 ECONNABORTED인 건 방화벽에 막혔거나 잘못된 주소를 요청한 경우
- 401: 인증 정보 없음(누구세요?) / * refresh token 해줘야함
- 403: 권한 없음(누군진 알겠는데 여기 오면 안됨) (forbidden)
- error.config 에는 에러난 요청의 정보들이 들어있어서 토큰 재발급 후에 실패했던 정보로 다시 요청을 보내준다.
- 그래도 실패하면 다시 로그인 필요
❗️위 코드는 예시 코드로, 잘못하면 무한루프가 돌기 때문에 추가 조치를 해줘야함 (refreshToken에서 access token이 전달이 잘 안됐으면 또 401이되서 무한루프가 됨) -> retry count를 만들거나, 1번밖에 못하는 식으로 해결
Refresh Token으로 access token 재발급하기
1. 로컬 스토리지에 리프레쉬 토큰 저장
2. 세션에 리프레쉬 토큰 저장
Refresh Token rotation
- 토큰이 스토리지에 저장되어 있어서 만료될 때까지 탈취자가 쓸 수 있는 경우: **"아직 만료안됐는데 왜 재발급해달라고함?"**이라고 서비스가 막을 수 있다.
- 리프레쉬 토큰을 1회용으로 만들어서 한번 사용하면 매번 새로 발급하는 방법이 있다: 털리고 나서 리프레쉬가 되버리면 **이미 사용한 토큰인데 왜가져옴? **하면서 막을 수 있음. <<-- refresh token rotation 방법으로, 리프레쉬 토큰은 1회용이고, 리프레쉬 할 때마다 새로운 access token과 새로운 refresh token이 넘어옴
- --> 유저가 사용을 하다가 자연스럽게 리프레쉬 되면서 탈취를 막을 수 있다.
🔗 프리온보딩 학습내용입니다.