[Challenge] 로그인 구현 lv.2

KoEunseo·2023년 3월 13일
0

challenge

목록 보기
3/9

키워드

react
react-router-dom
typescript
Token
JWT(Json Web Token)
Refresh Token

API

로그인 POST

url : 'https://url.com/auth/login;
body: { username: string, password: string }
response: { access_token: string }

유저정보 GET

url : 'https://url.com/profile'
header Authorization: Bearer ${access_token} 포함
response: { access_token: string }

api

const.ts

여기에 서버 url을 넣었는데 원래는 환경변수로 넣을것이다.

type

type LoginResult = "success" | "fail";
export type LoginResultWithToken =
  | {
      result: "success";
      access_token: string;
    }
  | {
      result: "fail";
      access_token: null;
    };

export interface LoginRequest {
  username: string;
  password: string;
}

utils

로컬스토리지에서 데이터를 세팅하고 받아오는 함수를 따로 만들어놓는다.
로컬에 저장하는 키값은 'accessToekn'이라는 것을 기억해두어야한다.
나처럼 헷갈려서 뻘짓할수있음...
서버에서 들어오는 토큰 키값은 'access_token'이기때문...ㅎㅎ
보면 주로 서버에서는 snake case, 프론트에서는 camel case로 표기하는 것 같음.(아닐수도있음)

export const saveAccessToken = (accessToken: string) => {
  localStorage.setItem('accessToken', accessToken)
}

export const getAccessToken = (): string => {
  return localStorage.getItem('accessToken') || ''
}

login : 1. 함수에서 토큰 직접 주입받아 쓰기

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;
};

login : 2. 로컬스토리지에 저장해 토큰 쓰기

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;
};

JWTLogin : 1. 함수로 토큰 주입해 로그인

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);
  };
};

JWTLogin : 2. 로컬스토리지에서 받아서 로그인(자동로그인)

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을 사용하면 최적화가 가능하다.

(+) JWT 보관방식과 보안

1. 재접근할 수 없는 런타임 메모리에 토큰을 저장하는 방법

비교적 안전. 그러나 사용자는 매번 로그인을 해야한다.

const userInfo = await getCurrentUserInfo(loginRes.access_token);

2. 로컬스토리지/쿠키에 저장

자동로그인을 통해 ux에 좋은 영향을 미치겠지만 탈취 위험이 높아진다.

3. accessToken & refreshToken

리프레시 토큰을 함께 생성해 보낸다.
액세스토큰은 유효시간을 짧게 해 로컬에 발급하고
리프레시토큰은 HttpOnly Cookie로 발급해 js로 접근이 불가능하도록 한다.
유저가 리프레시토큰이 살아있는데 재로그인을 하려고 한다면 모든 토큰을 무효화하고 다시 로그인하도록 유도한다.

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글