
--- client/resources/api/index.js
+++ client/resources/api/index.js
... | ... | @@ -1,33 +1,78 @@ |
1 | 1 |
import axios from 'axios'; |
2 | 2 |
import store from "../../views/pages/AppStore"; |
3 | 3 |
|
4 |
-// 토큰 재발급 함수 (공통 함수로 분리) |
|
5 |
-const refreshToken = async () => { |
|
6 |
- const res = await axios.post("/refresh/tknReissue.json", {}, { |
|
7 |
- headers: { |
|
8 |
- "Content-Type": "application/json; charset=UTF-8", |
|
9 |
- }, |
|
10 |
- }); |
|
11 |
- |
|
12 |
- if (res.status === 200) { |
|
13 |
- console.log("토큰 재발급 성공! 굿~"); |
|
14 |
- // 새로 발급 받은 AccessToken 저장 |
|
15 |
- store.commit('setAuthorization', res.headers.authorization); |
|
4 |
+// JWT 토큰의 만료 시간 확인 함수 |
|
5 |
+const isTokenExpired = () => { |
|
6 |
+ try { |
|
7 |
+ const token = store.state.authorization; |
|
8 |
+ if (!token) return true; |
|
16 | 9 |
|
17 | 10 |
// JWT 토큰 디코딩 |
18 |
- const base64String = store.state.authorization.split('.')[1]; |
|
11 |
+ const base64String = token.split('.')[1]; |
|
19 | 12 |
const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
20 | 13 |
const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { |
21 | 14 |
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
22 | 15 |
}).join('')); |
23 |
- const mbr = JSON.parse(jsonPayload); |
|
24 |
- store.commit("setUserNm", mbr.userNm); // 사용자 이름 저장 |
|
25 |
- store.commit('setRoles', mbr.roles); // 사용자 역할 저장 |
|
26 | 16 |
|
27 |
- return true; |
|
17 |
+ const payload = JSON.parse(jsonPayload); |
|
18 |
+ // exp는 토큰 만료 시간(Unix timestamp) |
|
19 |
+ const expTime = payload.exp * 1000; // 밀리초 단위로 변환 |
|
20 |
+ const currentTime = new Date().getTime(); |
|
21 |
+ |
|
22 |
+ // 토큰 만료 5분 전부터는 미리 갱신 |
|
23 |
+ return currentTime > (expTime - 5 * 60 * 1000); |
|
24 |
+ } catch (e) { |
|
25 |
+ console.error("토큰 검증 오류:", e); |
|
26 |
+ return true; // 오류 발생 시 만료된 것으로 간주 |
|
28 | 27 |
} |
29 |
- |
|
30 |
- throw new Error("토큰 재발급 요청 실패"); |
|
28 |
+}; |
|
29 |
+ |
|
30 |
+// 토큰 재발급 함수 (공통 함수로 분리) |
|
31 |
+const refreshToken = async () => { |
|
32 |
+ try { |
|
33 |
+ const res = await axios.post("/refresh/tknReissue.json", {}, { |
|
34 |
+ headers: { |
|
35 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
36 |
+ }, |
|
37 |
+ }); |
|
38 |
+ |
|
39 |
+ if (res.status === 200) { |
|
40 |
+ console.log("토큰 재발급 성공! 굿~"); |
|
41 |
+ // 새로 발급 받은 AccessToken 저장 |
|
42 |
+ store.commit('setAuthorization', res.headers.authorization); |
|
43 |
+ |
|
44 |
+ // JWT 토큰 디코딩 |
|
45 |
+ const base64String = store.state.authorization.split('.')[1]; |
|
46 |
+ const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
|
47 |
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { |
|
48 |
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
|
49 |
+ }).join('')); |
|
50 |
+ const mbr = JSON.parse(jsonPayload); |
|
51 |
+ store.commit("setUserNm", mbr.userNm); // 사용자 이름 저장 |
|
52 |
+ store.commit('setRoles', mbr.roles); // 사용자 역할 저장 |
|
53 |
+ |
|
54 |
+ return true; |
|
55 |
+ } |
|
56 |
+ |
|
57 |
+ throw new Error("토큰 재발급 요청 실패"); |
|
58 |
+ } catch (error) { |
|
59 |
+ console.error("토큰 재발급 중 오류:", error); |
|
60 |
+ throw error; |
|
61 |
+ } |
|
62 |
+}; |
|
63 |
+ |
|
64 |
+// 토큰 유효성 확인 및 필요시 갱신하는 함수 |
|
65 |
+const ensureValidToken = async () => { |
|
66 |
+ if (isTokenExpired()) { |
|
67 |
+ try { |
|
68 |
+ await refreshToken(); |
|
69 |
+ return true; |
|
70 |
+ } catch (error) { |
|
71 |
+ console.error("토큰 갱신 실패:", error); |
|
72 |
+ throw error; |
|
73 |
+ } |
|
74 |
+ } |
|
75 |
+ return true; |
|
31 | 76 |
}; |
32 | 77 |
|
33 | 78 |
// Axios 클라이언트 생성 함수 |
... | ... | @@ -40,10 +85,24 @@ |
40 | 85 |
|
41 | 86 |
// 요청 인터셉터 |
42 | 87 |
client.interceptors.request.use( |
43 |
- config => { |
|
44 |
- const token = store.state.authorization; // Access Token 가져오기 |
|
45 |
- if (token) { |
|
46 |
- config.headers.Authorization = token; // 토큰 추가 |
|
88 |
+ async config => { |
|
89 |
+ // 요청 전에 토큰 만료 확인 및 필요시 갱신 |
|
90 |
+ try { |
|
91 |
+ await ensureValidToken(); |
|
92 |
+ const token = store.state.authorization; |
|
93 |
+ if (token) { |
|
94 |
+ config.headers.Authorization = token; |
|
95 |
+ } |
|
96 |
+ } catch (error) { |
|
97 |
+ // 토큰 갱신 실패 시 로그인 페이지로 이동 |
|
98 |
+ if (!window.location.pathname.includes('/login')) { |
|
99 |
+ const redirect = window.location.pathname + window.location.search; |
|
100 |
+ sessionStorage.setItem("redirect", redirect); |
|
101 |
+ alert('세션이 종료되었습니다.\n로그인을 새로 해주세요.'); |
|
102 |
+ store.commit("setStoreReset"); |
|
103 |
+ window.location = '/login.page'; |
|
104 |
+ } |
|
105 |
+ throw error; |
|
47 | 106 |
} |
48 | 107 |
return config; |
49 | 108 |
}, |
... | ... | @@ -76,20 +135,29 @@ |
76 | 135 |
originalReq._retry = true; // 재시도 플래그 설정 |
77 | 136 |
|
78 | 137 |
try { |
79 |
- // 공통 토큰 재발급 함수 호출 |
|
138 |
+ // 토큰 재발급 |
|
80 | 139 |
await refreshToken(); |
81 | 140 |
|
82 |
- // 중요: Content-Type 헤더 보존 |
|
141 |
+ // 새 토큰으로 헤더 업데이트 |
|
83 | 142 |
originalReq.headers.Authorization = store.state.authorization; |
84 | 143 |
|
85 |
- // multipart/form-data 요청의 경우 data 처리 방식이 다름 |
|
144 |
+ // multipart/form-data 요청 처리를 위한 특별 처리 |
|
86 | 145 |
if (originalReq.headers['Content-Type'] && |
87 | 146 |
originalReq.headers['Content-Type'].includes('multipart/form-data')) { |
88 |
- // multipart의 경우 FormData가 이미 생성되어 있으므로 그대로 사용 |
|
89 |
- // 특별한 처리 없이 원래 요청 재시도 |
|
147 |
+ |
|
148 |
+ // 완전히 새로운 요청 생성 |
|
149 |
+ return axios({ |
|
150 |
+ ...originalReq, |
|
151 |
+ headers: { |
|
152 |
+ ...originalReq.headers, |
|
153 |
+ Authorization: store.state.authorization |
|
154 |
+ }, |
|
155 |
+ // FormData가 변경되지 않도록 원본 데이터 유지 |
|
156 |
+ transformRequest: [(data) => data] |
|
157 |
+ }); |
|
90 | 158 |
} |
91 | 159 |
|
92 |
- // 원래 요청 재시도 |
|
160 |
+ // 일반 요청은 기존 client 사용하여 재시도 |
|
93 | 161 |
return client(originalReq); |
94 | 162 |
} catch (refreshError) { |
95 | 163 |
const redirect = window.location.pathname + window.location.search; |
... | ... | @@ -114,4 +182,4 @@ |
114 | 182 |
// 멀티파트 파일 업로드를 위한 fileClient |
115 | 183 |
const fileClient = createClient('multipart/form-data'); |
116 | 184 |
|
117 |
-export { apiClient, fileClient }; // 두 클라이언트를 내보냄(파일 끝에 줄바꿈 문자 없음) |
|
185 |
+export { apiClient, fileClient, ensureValidToken }; // 두 클라이언트와 토큰 유효성 확인 함수 내보냄(파일 끝에 줄바꿈 문자 없음) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?