react
react-router-dom
typescript
Token
JWT(Json Web Token)
Refresh Token
url : 'https://url.com/auth/login;
body: { username: string, password: string }
response: { access_token: string }
url : 'https://url.com/profile'
header Authorization: Bearer ${access_token}
포함
response: { access_token: string }
여기에 서버 url을 넣었는데 원래는 환경변수로 넣을것이다.
type LoginResult = "success" | "fail";
export type LoginResultWithToken =
| {
result: "success";
access_token: string;
}
| {
result: "fail";
access_token: null;
};
export interface LoginRequest {
username: string;
password: string;
}
로컬스토리지에서 데이터를 세팅하고 받아오는 함수를 따로 만들어놓는다.
로컬에 저장하는 키값은 'accessToekn'이라는 것을 기억해두어야한다.
나처럼 헷갈려서 뻘짓할수있음...
서버에서 들어오는 토큰 키값은 'access_token'이기때문...ㅎㅎ
보면 주로 서버에서는 snake case, 프론트에서는 camel case로 표기하는 것 같음.(아닐수도있음)
export const saveAccessToken = (accessToken: string) => {
localStorage.setItem('accessToken', accessToken)
}
export const getAccessToken = (): string => {
return localStorage.getItem('accessToken') || ''
}
export const login = async (
args: LoginRequest
): Promise<LoginResultWithToken> => {
const res = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(args),
});
if (res.ok) {
const loginData = await res.json();
return {
result: "success",
access_token: loginData.access_token,
};
}
return {
result: "fail",
access_token: null,
};
};
export const getCurrentUserInfo = async (
token: string // 함수에서 토큰을 직접 주입받아 사용.
): Promise<UserInfo | null> => {
const res = await fetch(`${BASE_URL}/profile`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
// token을 Authorization header에 Bearer token으로 넣어준다.
},
});
if (res.ok) {
return res.json() as Promise<UserInfo>;
}
return null;
};
export const login = async (args: LoginRequest): Promise<LoginResult> => {
const res = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(args),
});
if (res.ok) {
const loginData = await res.json(); saveAccessToken(loginData.access_token);
return "success";
}
return "fail";
};
// 로컬스토리지에서 토큰 가져와 쓰기
// ** 나는 보통 로컬에서 가져오든 서버에서 가져오든 변수에 담아서 쓰는데, 그럴경우 어떤 경우에는 문제가 생길수도 있다고 했다. 무슨문제인지는... 찾아보자... **
export const getCurrentUserInfo = async (): Promise<UserInfo | null> => {
const res = await fetch(`${BASE_URL}/profile`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getAccessToken()}`,
},
});
if (res.ok) {
return res.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 loginPayload = {
username: formData.get("username") as string,
password: formData.get("password") as string,
};
const loginRes = await login(loginPayload);
if (loginRes.result === "fail") return;
const userInfo = await getCurrentUserInfo(loginRes.access_token);
if (userInfo === null) return;
setUserInfo(userInfo);
};
};
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 loginPayload = {
username: formData.get("username") as string,
password: formData.get("password") as string,
};
const loginRes = await login(loginPayload);
if (loginRes === "fail") return;
const userInfo = await getCurrentUserInfo(); //토큰을 직접 전달하지 않고 함수만 호출한다.
if (userInfo === null) return;
setUserInfo(userInfo);
};
};
이때 useCallback을 사용하면 최적화가 가능하다.
런타임 메모리
에 토큰을 저장하는 방법비교적 안전. 그러나 사용자는 매번 로그인을 해야한다.
const userInfo = await getCurrentUserInfo(loginRes.access_token);
자동로그인을 통해 ux에 좋은 영향을 미치겠지만 탈취 위험이 높아진다.
accessToken & refreshToken
리프레시 토큰을 함께 생성해 보낸다.
액세스토큰은 유효시간을 짧게 해 로컬에 발급
하고
리프레시토큰은 HttpOnly Cookie
로 발급해 js로 접근이 불가능하도록 한다.
유저가 리프레시토큰이 살아있는데 재로그인을 하려고 한다면 모든 토큰을 무효화
하고 다시 로그인하도록 유도한다.