

250604 김혜민 소셜 중복로그인 에러 수정
@70581945710691ba939a6cf7f5b456f38e5f20a5
--- client/resources/api/index.js
+++ client/resources/api/index.js
... | ... | @@ -1,12 +1,12 @@ |
1 | 1 |
import axios from 'axios'; |
2 |
-// import store from "../../views/pages/AppStore"; |
|
3 | 2 |
import { getGlobalStore } from '../../views/pages/AppStore'; |
4 | 3 |
|
5 | 4 |
const apiClient = axios.create({ |
6 | 5 |
baseURL: '/', |
7 | 6 |
headers: { |
8 | 7 |
'Content-Type': 'application/json; charset=UTF-8', |
9 |
- } |
|
8 |
+ }, |
|
9 |
+ withCredentials: true // 세션 쿠키를 위해 추가 |
|
10 | 10 |
}); |
11 | 11 |
|
12 | 12 |
const excludeCtxUrls = [ |
... | ... | @@ -15,15 +15,21 @@ |
15 | 15 |
|
16 | 16 |
apiClient.interceptors.request.use( |
17 | 17 |
config => { |
18 |
- const store = getGlobalStore(); // 전역 스토어 가져오기 |
|
18 |
+ const store = getGlobalStore(); |
|
19 | 19 |
const excludeCtxUrl = excludeCtxUrls.some(url => config.url.includes(url)); |
20 | 20 |
const contextPath = store?.state.contextPath || ''; |
21 |
- console.log("Context Path:", contextPath); |
|
21 |
+ const loginMode = store?.state.loginMode || 'J'; // 기본값은 JWT 모드 |
|
22 | 22 |
if(!excludeCtxUrl) { |
23 |
- config.url = contextPath + config.url; // 요청 시 Context Path 추가 |
|
23 |
+ config.url = contextPath + config.url; |
|
24 | 24 |
} |
25 | 25 |
|
26 |
- config.headers.Authorization = store?.state.authorization; // 요청 시 AccessToken 추가 |
|
26 |
+ // 로그인 모드에 따른 헤더 설정 |
|
27 |
+ if (loginMode === 'J') { |
|
28 |
+ config.headers.Authorization = store?.state.authorization; |
|
29 |
+ } else if (loginMode === 'S') { |
|
30 |
+ delete config.headers.Authorization; |
|
31 |
+ config.withCredentials = true; |
|
32 |
+ } |
|
27 | 33 |
return config; |
28 | 34 |
}, |
29 | 35 |
error => { |
... | ... | @@ -32,7 +38,6 @@ |
32 | 38 |
) |
33 | 39 |
|
34 | 40 |
apiClient.interceptors.response.use( |
35 |
- |
|
36 | 41 |
response => { |
37 | 42 |
return response; |
38 | 43 |
}, |
... | ... | @@ -40,22 +45,36 @@ |
40 | 45 |
if (!error.response) { |
41 | 46 |
return Promise.reject(error); |
42 | 47 |
} |
48 |
+ |
|
43 | 49 |
if (error.response.status == 403 && error.response.data.message == '접근 권한이 없습니다.') { |
44 | 50 |
window.history.back(); |
45 | 51 |
} |
52 |
+ |
|
46 | 53 |
const originalReq = error.config; |
54 |
+ const store = getGlobalStore(); |
|
55 |
+ const loginMode = store?.state.loginMode || 'J'; |
|
56 |
+ |
|
47 | 57 |
if (originalReq.url.includes('/refresh/tokenReissue.json')) { |
48 | 58 |
return Promise.reject(error); |
49 | 59 |
} |
50 |
- // 토큰의 만료기간이 끝난경우 |
|
51 |
- // if (error.response.status == 401 && error.response.data.message == 'Token expired' && !originalReq._retry) { |
|
52 |
- if (error.response.status === 401 && error.response.data?.message?.toLowerCase().includes('expired') && !originalReq._retry) { |
|
53 |
- const store = getGlobalStore(); // 전역 스토어 가져오기 |
|
54 |
- originalReq._retry = true; // 재요청 시도(한번만 실행) |
|
60 |
+ |
|
61 |
+ // 핵심: 401 에러 처리를 로그인 모드별로 분기 |
|
62 |
+ if (error.response.status === 401 && |
|
63 |
+ error.response.data?.message?.toLowerCase().includes('expired') && |
|
64 |
+ !originalReq._retry) { |
|
65 |
+ |
|
66 |
+ originalReq._retry = true; |
|
67 |
+ |
|
68 |
+ if (loginMode === 'J') { |
|
69 |
+ // JWT 모드: 기존 토큰 재발급 로직 |
|
55 | 70 |
try { |
56 |
- const res = await axios.post('/refresh/tokenReissue.json', {}); |
|
57 |
- store.commit('setAuthorization', res.headers.authorization); // 새로 발급 받은 AccessToken 저장 |
|
58 |
- originalReq.headers.Authorization = store.state.authorization; // 새로 발급 받은 AccessToken을 기존 요청에 추가 |
|
71 |
+ const res = await axios.post('/refresh/tokenReissue.json', {}, { |
|
72 |
+ withCredentials: true |
|
73 |
+ }); |
|
74 |
+ |
|
75 |
+ store.commit('setAuthorization', res.headers.authorization); |
|
76 |
+ originalReq.headers.Authorization = store.state.authorization; |
|
77 |
+ |
|
59 | 78 |
/** jwt토큰 디코딩 **/ |
60 | 79 |
const base64String = store.state.authorization.split('.')[1]; |
61 | 80 |
const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
... | ... | @@ -63,22 +82,39 @@ |
63 | 82 |
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
64 | 83 |
}).join('')); |
65 | 84 |
const mbr = JSON.parse(jsonPayload); |
66 |
- // const mbr = JSON.parse(decodeURIComponent(escape(window.atob(base64String)))); // jwt claim 추출 |
|
85 |
+ |
|
67 | 86 |
store.commit("setMbrNm", mbr.mbrNm); |
68 | 87 |
store.commit('setRoles', mbr.roles); |
69 |
- /** jwt토큰 디코딩 **/ |
|
70 |
- return apiClient(originalReq); // 원래 요청 재시도 /pathname + search |
|
88 |
+ /** jwt토큰 디코딩 끝 **/ |
|
89 |
+ |
|
90 |
+ return apiClient(originalReq); |
|
71 | 91 |
} catch (refreshError) { |
72 |
- const redirect = window.location.pathname + window.location.search; |
|
73 |
- sessionStorage.setItem("redirect", redirect); |
|
74 |
- alert('세션이 종료 되었습니다.\n로그인을 새로 해주세요.'); |
|
75 |
- store.commit("setStoreReset"); |
|
76 |
- window.location = '/login.page'; |
|
92 |
+ handleSessionExpired(store); |
|
77 | 93 |
return Promise.reject(refreshError); |
78 | 94 |
} |
95 |
+ } else if (loginMode === 'S') { |
|
96 |
+ // 세션 모드: 토큰 재발급 시도하지 않고 바로 로그인 페이지로 |
|
97 |
+ handleSessionExpired(store, '세션이 만료되었습니다.'); |
|
98 |
+ return Promise.reject(error); |
|
79 | 99 |
} |
100 |
+ } |
|
101 |
+ |
|
80 | 102 |
return Promise.reject(error); |
81 | 103 |
} |
82 | 104 |
) |
83 | 105 |
|
106 |
+// 세션 만료 처리 함수 |
|
107 |
+function handleSessionExpired(store, message = '세션이 종료되었습니다.') { |
|
108 |
+ const redirect = window.location.pathname + window.location.search; |
|
109 |
+ sessionStorage.setItem("redirect", redirect); |
|
110 |
+ alert(`${message}\n로그인을 새로 해주세요.`); |
|
111 |
+ store.commit("setStoreReset"); |
|
112 |
+ |
|
113 |
+ const admPath = store.state.path?.includes("/adm"); |
|
114 |
+ const contextPath = store.state.contextPath || ''; |
|
115 |
+ const loginUrl = admPath ? contextPath + "/cmslogin.page" : contextPath + "/login.page"; |
|
116 |
+ |
|
117 |
+ window.location = loginUrl; |
|
118 |
+} |
|
119 |
+ |
|
84 | 120 |
export default apiClient;(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -179,13 +179,21 @@ |
179 | 179 |
return; |
180 | 180 |
} |
181 | 181 |
|
182 |
- // 로그인 모드 확인 개선 |
|
183 |
- let loginMode = store.state.loginMode || localStorage.getItem('loginMode') || 'J'; // 기본값으로 JWT 설정 |
|
182 |
+ // 로그인 모드 확인 |
|
183 |
+ let loginMode = store.state.loginMode; |
|
184 | 184 |
|
185 | 185 |
// 로그인 모드가 여전히 없으면 localStorage에서 다시 한번 확인 |
186 |
- if (!loginMode || loginMode === 'undefined') { |
|
187 |
- loginMode = 'J'; |
|
188 |
- } |
|
186 |
+ if (!loginMode || loginMode === 'undefined') { |
|
187 |
+ // 스토어에 없으면 localStorage 확인 |
|
188 |
+ loginMode = localStorage.getItem('loginMode') || sessionStorage.getItem('loginMode'); |
|
189 |
+ } |
|
190 |
+ if (!loginMode || loginMode === 'undefined') { |
|
191 |
+ // 여전히 없으면 기본값 설정 |
|
192 |
+ loginMode = 'J'; |
|
193 |
+ } |
|
194 |
+ if (store.state.loginMode !== loginMode) { |
|
195 |
+ store.commit('setLoginMode', loginMode); |
|
196 |
+ } |
|
189 | 197 |
|
190 | 198 |
// 로그인 상태 확인 개선 |
191 | 199 |
let isLogin = false; |
... | ... | @@ -194,9 +202,10 @@ |
194 | 202 |
const token = store.state.authorization || localStorage.getItem('authorization'); |
195 | 203 |
isLogin = !!token; |
196 | 204 |
} else if (loginMode === 'S') { |
197 |
- // 세션 모드: mbrId 확인 |
|
198 |
- const mbrId = store.state.mbrId || localStorage.getItem('mbrId'); |
|
199 |
- isLogin = !!mbrId; |
|
205 |
+ // 세션 모드: mbrId 또는 mbrNm 확인 |
|
206 |
+ const mbrId = store.state.mbrId || localStorage.getItem('mbrId') || sessionStorage.getItem('mbrId'); |
|
207 |
+ const mbrNm = store.state.mbrNm || localStorage.getItem('mbrNm') || sessionStorage.getItem('mbrNm'); |
|
208 |
+ isLogin = !!(mbrId || mbrNm); |
|
200 | 209 |
} |
201 | 210 |
|
202 | 211 |
// OAuth2 콜백 처리 - 로그인 페이지에서만 처리 |
--- client/views/pages/AppStore.js
+++ client/views/pages/AppStore.js
... | ... | @@ -17,13 +17,11 @@ |
17 | 17 |
export default function createAppStore(ctx, strgMode, lgnMode) { |
18 | 18 |
const store = createStore({ |
19 | 19 |
plugins: [createPersistedState({ |
20 |
- // storage: window.sessionStorage, |
|
21 | 20 |
storage: strgMode === 'S' ? window.sessionStorage : window.localStorage, |
22 | 21 |
paths: ['loginMode', 'authorization', 'mbrId', 'mbrNm', 'roles', 'contextPath', 'pageAuth'] |
23 | 22 |
})], |
24 | 23 |
state: { |
25 | 24 |
authorization: null, |
26 |
- // refresh: null, |
|
27 | 25 |
loginMode: lgnMode || 'J', |
28 | 26 |
userType: "portal", |
29 | 27 |
menu: null, |
... | ... | @@ -34,18 +32,23 @@ |
34 | 32 |
}, |
35 | 33 |
getters: { |
36 | 34 |
getAuthorization: function () {}, |
37 |
- // getRefresh: function () {}, |
|
38 | 35 |
getMbrNm: function () {}, |
39 | 36 |
getRoles: function () {}, |
40 | 37 |
getLoginMode: state => state.loginMode, |
38 |
+ // 로그인 상태 확인 |
|
39 |
+ isLoggedIn: (state) => { |
|
40 |
+ if (state.loginMode === 'J') { |
|
41 |
+ return !!state.authorization && !!state.mbrNm; |
|
42 |
+ } else if (state.loginMode === 'S') { |
|
43 |
+ return !!state.mbrNm; |
|
44 |
+ } |
|
45 |
+ return false; |
|
46 |
+ }, |
|
41 | 47 |
}, |
42 | 48 |
mutations: { |
43 | 49 |
setAuthorization(state, newValue) { |
44 | 50 |
state.authorization = newValue; |
45 | 51 |
}, |
46 |
- // setRefresh(state, newValue) { |
|
47 |
- // state.refresh = newValue; |
|
48 |
- // }, |
|
49 | 52 |
setMbrNm(state, newValue) { |
50 | 53 |
state.mbrNm = newValue; |
51 | 54 |
}, |
... | ... | @@ -66,7 +69,6 @@ |
66 | 69 |
}, |
67 | 70 |
setStoreReset(state) { |
68 | 71 |
state.authorization = null; |
69 |
- // state.refresh = null; |
|
70 | 72 |
state.loginMode = 'J'; |
71 | 73 |
state.mbrNm = null; |
72 | 74 |
state.mbrId = null; |
... | ... | @@ -102,12 +104,29 @@ |
102 | 104 |
setLoginMode(state, value) { |
103 | 105 |
state.loginMode = value; |
104 | 106 |
}, |
107 |
+ // 로그인 정보 일괄 설정 |
|
108 |
+ setLoginInfo(state, { mbrNm, mbrId, roles, authorization, loginMode }) { |
|
109 |
+ state.mbrNm = mbrNm; |
|
110 |
+ state.mbrId = mbrId; |
|
111 |
+ state.roles = roles || [{authority: "ROLE_NONE"}]; |
|
112 |
+ |
|
113 |
+ if (loginMode) { |
|
114 |
+ state.loginMode = loginMode; |
|
115 |
+ } |
|
116 |
+ |
|
117 |
+ // JWT 모드일 때만 authorization 저장 |
|
118 |
+ if (state.loginMode === 'J' && authorization) { |
|
119 |
+ state.authorization = authorization; |
|
120 |
+ } else if (state.loginMode === 'S') { |
|
121 |
+ state.authorization = null; // 세션 모드에서는 토큰 불필요 |
|
122 |
+ } |
|
123 |
+ }, |
|
105 | 124 |
}, |
106 | 125 |
actions: { |
107 | 126 |
async logout({ commit }) { |
108 | 127 |
try { |
109 |
- const ctx = this.state.contextPath; // 캐시 초기화 전 contextPath 저장 |
|
110 |
- const admPath = this.state.path?.includes("/adm") // 캐시 초기화 전 경로 구분 (true: 관리자 페이지, false: 사용자 페이지) |
|
128 |
+ const ctx = this.state.contextPath; |
|
129 |
+ const admPath = this.state.path?.includes("/adm") |
|
111 | 130 |
const loginMode = this.state.loginMode || localStorage.getItem('loginMode') || 'J'; |
112 | 131 |
|
113 | 132 |
// 로그인 모드에 따른 처리 |
... | ... | @@ -136,9 +155,9 @@ |
136 | 155 |
'refresh', |
137 | 156 |
'Authorization', |
138 | 157 |
'JSESSIONID', |
139 |
- 'oauth_access_token', // OAuth 토큰 쿠키 |
|
140 |
- 'oauth_refresh_token', // OAuth 리프레시 토큰 |
|
141 |
- 'SESSION' // 스프링 기본 세션 쿠키 |
|
158 |
+ 'oauth_access_token', |
|
159 |
+ 'oauth_refresh_token', |
|
160 |
+ 'SESSION' |
|
142 | 161 |
]; |
143 | 162 |
|
144 | 163 |
cookiesToDelete.forEach(cookieName => { |
... | ... | @@ -146,12 +165,11 @@ |
146 | 165 |
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`; |
147 | 166 |
}); |
148 | 167 |
|
149 |
- // 4. 로그인 페이지로 이동 (OAuth 관련 파라미터 제거) |
|
168 |
+ // 4. 로그인 페이지로 이동 |
|
150 | 169 |
const cleanUrl = admPath ? |
151 | 170 |
ctx + "/cmslogin.page" : |
152 | 171 |
ctx + "/login.page"; |
153 | 172 |
|
154 |
- // URL에서 OAuth 관련 파라미터 제거 |
|
155 | 173 |
window.history.replaceState({}, document.title, cleanUrl); |
156 | 174 |
window.location.href = cleanUrl; |
157 | 175 |
|
... | ... | @@ -166,7 +184,6 @@ |
166 | 184 |
const ctx = this.state.contextPath; |
167 | 185 |
const admPath = this.state.path?.includes("/adm"); |
168 | 186 |
|
169 |
- // 에러 메시지 표시 |
|
170 | 187 |
const errorData = error.response?.data; |
171 | 188 |
if (errorData?.message) { |
172 | 189 |
alert(errorData.message); |
... | ... | @@ -174,7 +191,6 @@ |
174 | 191 |
console.log("로그아웃 처리 중 예상치 못한 오류 발생"); |
175 | 192 |
} |
176 | 193 |
|
177 |
- // 로그인 페이지로 이동 |
|
178 | 194 |
const cleanUrl = admPath ? |
179 | 195 |
ctx + "/cmslogin.page" : |
180 | 196 |
ctx + "/login.page"; |
... | ... | @@ -200,7 +216,7 @@ |
200 | 216 |
}, |
201 | 217 |
}); |
202 | 218 |
|
203 |
- setGlobalStore(store); // 전역 스토어 설정 |
|
219 |
+ setGlobalStore(store); |
|
204 | 220 |
|
205 | 221 |
return store; |
206 | 222 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/login/AdminLogin.vue
+++ client/views/pages/login/AdminLogin.vue
... | ... | @@ -185,7 +185,7 @@ |
185 | 185 |
|
186 | 186 |
// 로그인 성공 시 |
187 | 187 |
async loginSuccessProc(res) { |
188 |
- const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분 |
|
188 |
+ const loginType = res.headers['loginmode']; // 세션/토큰 로그인 구분 |
|
189 | 189 |
this.storageType = res.headers['storage-type']; // 로컬 스토리지 타입 (세션/로컬) |
190 | 190 |
if (loginType === 'J') { |
191 | 191 |
this.handleJWTLogin(res); |
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
... | ... | @@ -155,7 +155,9 @@ |
155 | 155 |
}, |
156 | 156 |
|
157 | 157 |
async loginSuccessProc(res) { |
158 |
- const loginType = res.headers['login-type']; |
|
158 |
+ console.log('login success :: res? = ' , res); |
|
159 |
+ const loginType = res.headers['loginmode']; |
|
160 |
+ console.log('loginType : ', loginType); |
|
159 | 161 |
if (loginType === 'J') { |
160 | 162 |
this.handleJWTLogin(res); |
161 | 163 |
} else if (loginType === 'S') { |
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?