From ca1966f88468c65f4490278cd9c58fe857a258e7 Mon Sep 17 00:00:00 2001 From: jhj9109 Date: Tue, 19 Dec 2023 17:22:46 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Fix=20=EB=A1=9C=EC=BB=AC=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상수 관리 - expireDays 5일 삭제: JwtToken에 대한 expire는 이미 payload에 포함되어 있음 - 두 매직넘버를 상수로 표현 - JWT_PART_LENGTH : JWT 토큰은 헤더 | 페이로드 | 시그니처 3부분 - MS_PER_SEC : 1초 = 1000밀리초 Jwt 오브젝트가 아닌 토큰(string)만 보관 - expire가 기능적으로 중복되어 있음 - 토큰(string)만 저장토록 수정 - saveToekn & getToken 함수 인자나 반환형태가 변하지 않아 기존 코드와 호환 가능 saveToken() - 로컬스토리지에 저장하는 형태를 string으로 변경 clearToken() - 로컬스토리지 clear아닌 removeItem으로 수정 getToken() - 기존과 동일하게 동작 - 유효하지 않은 토큰은 로컬스토리지에서 삭제 & null 리턴 - 유효한 토큰만 string 리턴 --- src/cert/TokenStorage.ts | 82 ++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/src/cert/TokenStorage.ts b/src/cert/TokenStorage.ts index 3bc115e..8edcf57 100644 --- a/src/cert/TokenStorage.ts +++ b/src/cert/TokenStorage.ts @@ -1,28 +1,78 @@ const TOKEN = 'token'; -const expireDays = 5; + +const JWT_PART_LENGTH = 3; +const MS_PER_SEC = 1000; + +enum JWTPart { + Header, + Payload, + Signature, +} +export interface JWTPayload { + id: string; + isAdmin: number; // 또는 boolean, 상황에 따라 적절한 타입을 선택하세요. + iat: number; + exp: number; +} + +export function getJWTPart(token: string, part: JWTPart): string { + const parts = token.split('.'); + if (parts.length !== JWT_PART_LENGTH) { + throw new Error('Invalid JWT token format.'); + } + return parts[part]; +} + +export function isValidJWTPayload(object: any): object is JWTPayload { + return ( + object !== null && + typeof object === 'object' && + 'id' in object && + typeof object.id === 'string' && + 'isAdmin' in object && + typeof object.isAdmin === 'number' && + 'iat' in object && + typeof object.iat === 'number' && + 'exp' in object && + typeof object.exp === 'number' + ); +} + +export function parseJWTPayload(token: string): JWTPayload { + try { + const payload = JSON.parse(getJWTPart(token, JWTPart.Payload)); + if (!isValidJWTPayload(payload)) { + throw new Error('Invalid JWTPayload'); + } + return payload; + } catch (error) { + throw error; + } +} + +export function isJWTExpired(token: string): boolean { + try { + const payload = parseJWTPayload(token); + return payload.exp < Date.now() / MS_PER_SEC; + } catch (error) { + console.error(error); + return true; + } +} export function clearToken() { - localStorage.clear(); + localStorage.removeItem(TOKEN); } export function saveToken(token: string) { - const tokenObj = { - value: token, - expire: Date.now() + expireDays * 24 * 60 * 60 * 1000, - }; - const tokenObjString = JSON.stringify(tokenObj); - localStorage.setItem(TOKEN, tokenObjString); + localStorage.setItem(TOKEN, token); } export function getToken() { - const tokenObjString = localStorage.getItem(TOKEN); - if (!tokenObjString) { - return null; - } - const tokenObj = JSON.parse(tokenObjString); - if (Date.now() > tokenObj.expire) { + let token = localStorage.getItem(TOKEN); + if (token !== null && isJWTExpired(token)) { clearToken(); - return null; + token = null; } - return tokenObj.value; + return token; } From 6e65fbeb4c74de68b7253b56352d36d62d15eba8 Mon Sep 17 00:00:00 2001 From: Jiwon-Woo Date: Wed, 20 Dec 2023 12:27:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20JWTPayload=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20decodeTok?= =?UTF-8?q?en=EC=9C=BC=EB=A1=9C=20token=20=ED=8C=8C=EC=8B=B1=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- src/cert/AuthStorage.ts | 2 +- src/cert/TokenStorage.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/cert/AuthStorage.ts b/src/cert/AuthStorage.ts index edb1e4d..01dd5f3 100644 --- a/src/cert/AuthStorage.ts +++ b/src/cert/AuthStorage.ts @@ -14,7 +14,7 @@ export function removeAuth() { localStorage.removeItem(AUTH); } -const decodeToken = (token: string) => { +export const decodeToken = (token: string) => { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( diff --git a/src/cert/TokenStorage.ts b/src/cert/TokenStorage.ts index 8edcf57..ffca7f5 100644 --- a/src/cert/TokenStorage.ts +++ b/src/cert/TokenStorage.ts @@ -1,3 +1,5 @@ +import { decodeToken } from './AuthStorage'; + const TOKEN = 'token'; const JWT_PART_LENGTH = 3; @@ -10,7 +12,6 @@ enum JWTPart { } export interface JWTPayload { id: string; - isAdmin: number; // 또는 boolean, 상황에 따라 적절한 타입을 선택하세요. iat: number; exp: number; } @@ -28,9 +29,7 @@ export function isValidJWTPayload(object: any): object is JWTPayload { object !== null && typeof object === 'object' && 'id' in object && - typeof object.id === 'string' && - 'isAdmin' in object && - typeof object.isAdmin === 'number' && + typeof object.id === 'number' && 'iat' in object && typeof object.iat === 'number' && 'exp' in object && @@ -40,7 +39,7 @@ export function isValidJWTPayload(object: any): object is JWTPayload { export function parseJWTPayload(token: string): JWTPayload { try { - const payload = JSON.parse(getJWTPart(token, JWTPart.Payload)); + const payload = decodeToken(token); if (!isValidJWTPayload(payload)) { throw new Error('Invalid JWTPayload'); } From 23ffccc7b1418c91dce50ee0d980125cfac089f4 Mon Sep 17 00:00:00 2001 From: Jiwon-Woo Date: Wed, 20 Dec 2023 13:01:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20saveAuth,=20removeAuth=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20TokenStage=EC=99=80=20AuthStor?= =?UTF-8?q?age=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- src/cert/AuthStorage.ts | 28 ---------------------------- src/cert/TokenStorage.ts | 24 ++++++++++++++++++++++-- src/components/Auth/AuthCallback.tsx | 3 +-- src/components/Auth/AuthSignUp.tsx | 2 -- src/components/utils/Navbar.tsx | 2 -- 5 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 src/cert/AuthStorage.ts diff --git a/src/cert/AuthStorage.ts b/src/cert/AuthStorage.ts deleted file mode 100644 index 01dd5f3..0000000 --- a/src/cert/AuthStorage.ts +++ /dev/null @@ -1,28 +0,0 @@ -const AUTH = 'auth'; - -export function saveAuth(token: string) { - const decoded = decodeToken(token); - let userData: { id: string; url: string }; - userData = { id: decoded.username, url: decoded.imageUrl }; - localStorage.setItem(AUTH, JSON.stringify(userData)); -} -export function getAuth(): { id: string; url: string } { - return JSON.parse(localStorage.getItem(AUTH)); -} - -export function removeAuth() { - localStorage.removeItem(AUTH); -} - -export const decodeToken = (token: string) => { - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) - .join(''), - ); - - return JSON.parse(jsonPayload); -}; diff --git a/src/cert/TokenStorage.ts b/src/cert/TokenStorage.ts index ffca7f5..135d95c 100644 --- a/src/cert/TokenStorage.ts +++ b/src/cert/TokenStorage.ts @@ -1,5 +1,3 @@ -import { decodeToken } from './AuthStorage'; - const TOKEN = 'token'; const JWT_PART_LENGTH = 3; @@ -75,3 +73,25 @@ export function getToken() { } return token; } + +export function getAuth(): { id: string; url: string } { + const token = localStorage.getItem(TOKEN); + if (!token) { + return { id: null, url: null }; + } + const decoded = decodeToken(token); + return { id: decoded.username, url: decoded.imageUrl }; +} + +const decodeToken = (token: string) => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join(''), + ); + + return JSON.parse(jsonPayload); +}; diff --git a/src/components/Auth/AuthCallback.tsx b/src/components/Auth/AuthCallback.tsx index 47adde1..4bb02a8 100644 --- a/src/components/Auth/AuthCallback.tsx +++ b/src/components/Auth/AuthCallback.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { saveToken } from '@cert/TokenStorage'; import { useNavigate } from 'react-router-dom'; -import { saveAuth, getAuth } from '@cert/AuthStorage'; +import { getAuth } from '@cert/TokenStorage'; import GlobalLoginState from '@recoil/GlobalLoginState'; import { useSetRecoilState } from 'recoil'; @@ -15,7 +15,6 @@ const AuthCallback = () => { useEffect(() => { saveToken(token); - saveAuth(token); setLoginState(() => { return { id: getAuth().id, diff --git a/src/components/Auth/AuthSignUp.tsx b/src/components/Auth/AuthSignUp.tsx index 3bff2b8..82abc5b 100644 --- a/src/components/Auth/AuthSignUp.tsx +++ b/src/components/Auth/AuthSignUp.tsx @@ -5,7 +5,6 @@ import { useSetRecoilState, useRecoilValue } from 'recoil'; import errorAlert from '@globalObj/function/errorAlert'; import apiClient from '@service/apiClient'; import { saveToken } from '@cert/TokenStorage'; -import { saveAuth } from '@cert/AuthStorage'; import ProfileChangeModalShow from '@recoil/ProfileChangeModalShow'; import SignUpProfileState from '@recoil/SignUpProfileState'; import ProfileModal from '@auth/ProfileModal'; @@ -34,7 +33,6 @@ const AuthSignUp = () => { .then((res) => { console.log('success', res.data); saveToken(res.data.access_token); - saveAuth(res.data.access_token); setLoginState(() => { return { id: res.data.id, diff --git a/src/components/utils/Navbar.tsx b/src/components/utils/Navbar.tsx index d757580..5ae152a 100644 --- a/src/components/utils/Navbar.tsx +++ b/src/components/utils/Navbar.tsx @@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import GlobalLoginState from '@recoil/GlobalLoginState'; import { clearToken, getToken } from '@cert/TokenStorage'; -import { removeAuth } from '@cert/AuthStorage'; import { Dropdown } from './Dropdown'; import getAddress from '@globalObj/function/getAddress'; import apiClient from '@service/apiClient'; @@ -18,7 +17,6 @@ function Navbar() { apiClient .post('/auth/logout') .then((res) => { - removeAuth(); clearToken(); setLoginState(() => { return { From 8fe342c3b79ebed8be370ce985f03274859b2bbe Mon Sep 17 00:00:00 2001 From: Jiwon-Woo Date: Wed, 20 Dec 2023 13:01:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD=20getAuth=20->=20getDecodedToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- src/cert/TokenStorage.ts | 2 +- src/components/Auth/AuthCallback.tsx | 6 +++--- src/components/Review/SelectModal.tsx | 4 ++-- src/components/Rotation/Calendar.tsx | 4 ++-- src/components/Rotation/Rotation.tsx | 4 ++-- src/components/Rotation/RotationResult.tsx | 4 ++-- src/components/Rotation/event_utils.tsx | 4 ++-- src/globalObj/object/EmptyEvent.ts | 4 ++-- src/recoil/GlobalLoginState.ts | 10 +++++----- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/cert/TokenStorage.ts b/src/cert/TokenStorage.ts index 135d95c..38a0e2e 100644 --- a/src/cert/TokenStorage.ts +++ b/src/cert/TokenStorage.ts @@ -74,7 +74,7 @@ export function getToken() { return token; } -export function getAuth(): { id: string; url: string } { +export function getDecodedToken(): { id: string; url: string } { const token = localStorage.getItem(TOKEN); if (!token) { return { id: null, url: null }; diff --git a/src/components/Auth/AuthCallback.tsx b/src/components/Auth/AuthCallback.tsx index 4bb02a8..d2431aa 100644 --- a/src/components/Auth/AuthCallback.tsx +++ b/src/components/Auth/AuthCallback.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { saveToken } from '@cert/TokenStorage'; import { useNavigate } from 'react-router-dom'; -import { getAuth } from '@cert/TokenStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import GlobalLoginState from '@recoil/GlobalLoginState'; import { useSetRecoilState } from 'recoil'; @@ -17,10 +17,10 @@ const AuthCallback = () => { saveToken(token); setLoginState(() => { return { - id: getAuth().id, + id: getDecodedToken().id, isLogin: true, isAdmin: false, - profileUrl: getAuth().url, + profileUrl: getDecodedToken().url, }; }); navigate('/'); diff --git a/src/components/Review/SelectModal.tsx b/src/components/Review/SelectModal.tsx index 2d5c0df..d806442 100644 --- a/src/components/Review/SelectModal.tsx +++ b/src/components/Review/SelectModal.tsx @@ -12,7 +12,7 @@ import useSWR from 'swr'; import getAddress from '@globalObj/function/getAddress'; import fetcher from '@globalObj/function/fetcher'; import EmptyEvent from '@globalObj/object/EmptyEvent'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; function SelectModal(prop: { mode: string }) { const { data: eventList, mutate: mutateAllEvent } = useSWR( @@ -66,7 +66,7 @@ function SelectModal(prop: { mode: string }) { setSelectedTeam({ [event]: memArr }); setEventListModalShow(false); } else { - setSelectedTeam({ [event]: [{ intraId: getAuth().id, profile: getAuth().url, teamId: -1 }] }); + setSelectedTeam({ [event]: [{ intraId: getDecodedToken().id, profile: getDecodedToken().url, teamId: -1 }] }); setEventListModalShow(false); } }; diff --git a/src/components/Rotation/Calendar.tsx b/src/components/Rotation/Calendar.tsx index ce33353..6075ed1 100644 --- a/src/components/Rotation/Calendar.tsx +++ b/src/components/Rotation/Calendar.tsx @@ -10,7 +10,7 @@ import getAddress from '@globalObj/function/getAddress'; import { getToken } from '@cert/TokenStorage'; import errorAlert from '@globalObj/function/errorAlert'; import '@css/Rotation/Calendar.scss'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import { DAY_OF_SUNDAY } from './rotation_utils'; import apiClient from '@service/apiClient'; @@ -21,7 +21,7 @@ const COLOR = { export default class Calendar extends React.Component { state = { - auth: getAuth(), + auth: getDecodedToken(), weekendsVisible: true, currentEvents: [], }; diff --git a/src/components/Rotation/Rotation.tsx b/src/components/Rotation/Rotation.tsx index f25576c..e425bef 100644 --- a/src/components/Rotation/Rotation.tsx +++ b/src/components/Rotation/Rotation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import Calendar, { CalendarTileProperties } from 'react-calendar'; import getAddress from '@globalObj/function/getAddress'; import axios from 'axios'; @@ -325,7 +325,7 @@ export const Rotate = () => { const navigate = useNavigate(); const currentDate = new Date(); const initialRecord = createInitialObject(currentDate); - const intraId = getAuth()?.id ?? null; + const intraId = getDecodedToken()?.id ?? null; const isRotationApplicationPeriod = calculateIsRotationApplicationPeriod(currentDate); const [record, setRecord] = useState(() => ({ ...initialRecord })); const [isSubmit, setIsSumbit] = useState(false); diff --git a/src/components/Rotation/RotationResult.tsx b/src/components/Rotation/RotationResult.tsx index 5c03fa8..e1dc0fd 100644 --- a/src/components/Rotation/RotationResult.tsx +++ b/src/components/Rotation/RotationResult.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import LoadingSpinner from './Loading'; import { RotateUserResult } from './RotateUserResult'; import { getRotationMonthArr } from './event_utils'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import '@css/Rotation/Rotation.scss'; export const RotateResult = () => { @@ -13,7 +13,7 @@ export const RotateResult = () => { date < new Date(year, nextmonth, -1) ? (month = nextmonth - 1) : nextmonth; const [Loading, setLoading] = useState(true); const [arr, setArr] = useState([]); - const intraId = getAuth() ? getAuth().id : null; + const intraId = getDecodedToken() ? getDecodedToken().id : null; const mainApi = async () => { setLoading(true); // api 호출 전에 true로 변경하여 로딩화면 띄우기 diff --git a/src/components/Rotation/event_utils.tsx b/src/components/Rotation/event_utils.tsx index 23466fc..3ff5467 100644 --- a/src/components/Rotation/event_utils.tsx +++ b/src/components/Rotation/event_utils.tsx @@ -1,5 +1,5 @@ import getAddress from '@globalObj/function/getAddress'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import apiClient from '@service/apiClient'; import errorAlert from '@globalObj/function/errorAlert'; @@ -11,7 +11,7 @@ export function createEventId() { } function rotatedArrAllInfo(data) { - const intraId = getAuth() ? getAuth().id : null; + const intraId = getDecodedToken() ? getDecodedToken().id : null; return data .filter((el) => !!el.year && !!el.month && !!el.day) .map((el) => ({ diff --git a/src/globalObj/object/EmptyEvent.ts b/src/globalObj/object/EmptyEvent.ts index 2059be4..dc91ed5 100644 --- a/src/globalObj/object/EmptyEvent.ts +++ b/src/globalObj/object/EmptyEvent.ts @@ -1,4 +1,4 @@ -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import { ReviewSelectedEventType } from './types'; function EmptyEvent(): ReviewSelectedEventType { @@ -9,7 +9,7 @@ function EmptyEvent(): ReviewSelectedEventType { createdId: 1, intraId: 'tkim', isMatching: 1, - teamList: { 사서: [{ intraId: getAuth().id, profile: getAuth().url, teamId: -1 }] }, + teamList: { 사서: [{ intraId: getDecodedToken().id, profile: getDecodedToken().url, teamId: -1 }] }, }; } diff --git a/src/recoil/GlobalLoginState.ts b/src/recoil/GlobalLoginState.ts index 43a8838..b61401e 100644 --- a/src/recoil/GlobalLoginState.ts +++ b/src/recoil/GlobalLoginState.ts @@ -1,13 +1,13 @@ import { atom } from 'recoil'; import { userData } from '@globalObj/object/types'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; -const value = getAuth() +const value = getDecodedToken() ? { isLogin: true, - isAdmin: getAuth()['id'] === 'tkim', - id: getAuth()['id'], - profileUrl: getAuth()['url'], + isAdmin: getDecodedToken()['id'] === 'tkim', + id: getDecodedToken()['id'], + profileUrl: getDecodedToken()['url'], } : { isLogin: false,