

250526 김혜민 oauth2 추가
@bdf2e3f3825ce9ec5fe3250c8f72bba494b5b9e0
--- client/resources/api/login.js
+++ client/resources/api/login.js
... | ... | @@ -2,4 +2,19 @@ |
2 | 2 |
|
3 | 3 |
export const loginProc = mber => { |
4 | 4 |
return apiClient.post(`/mbr/loginProc.json`, mber); |
5 |
-}(파일 끝에 줄바꿈 문자 없음) |
|
5 |
+} |
|
6 |
+ |
|
7 |
+export const oauthLogin = (provider) => { |
|
8 |
+ const { API_SERVER_HOST } = require("../../../Global"); |
|
9 |
+ const oauthUrl = `//${API_SERVER_HOST}/oauth2/login?provider=${provider}`; |
|
10 |
+ window.location.href = oauthUrl; |
|
11 |
+}; |
|
12 |
+ |
|
13 |
+export const getOAuthUrl = (provider) => { |
|
14 |
+ const { API_SERVER_HOST } = require("../../../Global"); |
|
15 |
+ return `//${API_SERVER_HOST}/oauth2/login?provider=${provider}`; |
|
16 |
+}; |
|
17 |
+ |
|
18 |
+export const getOAuthUserInfo = () => { |
|
19 |
+ return apiClient.get(`/oauth2/user-info`); |
|
20 |
+};(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppStore.js
+++ client/views/pages/AppStore.js
... | ... | @@ -93,31 +93,71 @@ |
93 | 93 |
try { |
94 | 94 |
const ctx = this.state.contextPath; // 캐시 초기화 전 contextPath 저장 |
95 | 95 |
const admPath = this.state.path?.includes("/adm") // 캐시 초기화 전 경로 구분 (true: 관리자 페이지, false: 사용자 페이지) |
96 |
- const res = await logOutProc(); |
|
97 |
- alert(res.data.message); |
|
98 |
- if (res.status == 200) { |
|
99 |
- // 1. 상태 초기화 |
|
100 |
- commit("setStoreReset"); |
|
101 |
- // 2. 로컬스토리지와 세션스토리지 초기화 |
|
102 |
- localStorage.clear(); |
|
103 |
- sessionStorage.clear(); |
|
104 |
- |
|
105 |
- // 3. 쿠키 삭제 |
|
106 |
- document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
107 |
- document.cookie = "Authorization=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
108 |
- // 4. 로그인 페이지로 이동 |
|
109 |
- if(admPath) { |
|
110 |
- window.location = ctx + "/cmslogin.page"; |
|
111 |
- } else { |
|
112 |
- window.location = ctx + "/login.page"; |
|
96 |
+ const loginMode = this.state.loginMode; // 로그인 모드 확인 |
|
97 |
+ |
|
98 |
+ console.log("로그아웃 처리 시작 - 로그인 모드:", loginMode); |
|
99 |
+ |
|
100 |
+ // 로그인 모드에 따른 처리 |
|
101 |
+ if (loginMode === 'J') { |
|
102 |
+ // JWT 방식인 경우만 서버 API 호출 |
|
103 |
+ try { |
|
104 |
+ const res = await logOutProc(); |
|
105 |
+ alert(res.data.message); |
|
106 |
+ } catch (error) { |
|
107 |
+ console.log("JWT 로그아웃 API 에러, 클라이언트만 정리:", error); |
|
108 |
+ // API 에러가 발생해도 클라이언트는 정리 |
|
113 | 109 |
} |
114 |
- } |
|
115 |
- } catch(error) { |
|
116 |
- const errorData = error.response.data; |
|
117 |
- if (errorData.message != null && errorData.message != "") { |
|
118 |
- alert(error.response.data.message); |
|
119 | 110 |
} else { |
120 |
- alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
|
111 |
+ // 세션 방식 (OAuth 포함)은 서버 API 호출 없이 클라이언트만 정리 |
|
112 |
+ console.log("세션 방식 로그아웃 - 클라이언트만 정리"); |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ // 공통 클라이언트 정리 작업 |
|
116 |
+ // 1. 상태 초기화 |
|
117 |
+ commit("setStoreReset"); |
|
118 |
+ |
|
119 |
+ // 2. 로컬스토리지와 세션스토리지 초기화 |
|
120 |
+ localStorage.clear(); |
|
121 |
+ sessionStorage.clear(); |
|
122 |
+ |
|
123 |
+ // 3. 쿠키 삭제 |
|
124 |
+ document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
125 |
+ document.cookie = "Authorization=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
126 |
+ document.cookie = "JSESSIONID=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
127 |
+ |
|
128 |
+ console.log("로그아웃 완료"); |
|
129 |
+ |
|
130 |
+ // 4. 로그인 페이지로 이동 |
|
131 |
+ if(admPath) { |
|
132 |
+ window.location = ctx + "/cmslogin.page"; |
|
133 |
+ } else { |
|
134 |
+ window.location = ctx + "/login.page"; |
|
135 |
+ } |
|
136 |
+ |
|
137 |
+ } catch(error) { |
|
138 |
+ console.error("로그아웃 처리 중 오류:", error); |
|
139 |
+ |
|
140 |
+ // 에러가 발생해도 클라이언트 상태는 정리 |
|
141 |
+ commit("setStoreReset"); |
|
142 |
+ localStorage.clear(); |
|
143 |
+ sessionStorage.clear(); |
|
144 |
+ |
|
145 |
+ const ctx = this.state.contextPath; |
|
146 |
+ const admPath = this.state.path?.includes("/adm"); |
|
147 |
+ |
|
148 |
+ // 에러 메시지 표시 |
|
149 |
+ const errorData = error.response?.data; |
|
150 |
+ if (errorData?.message) { |
|
151 |
+ alert(errorData.message); |
|
152 |
+ } else { |
|
153 |
+ |
|
154 |
+ } |
|
155 |
+ |
|
156 |
+ // 로그인 페이지로 이동 |
|
157 |
+ if(admPath) { |
|
158 |
+ window.location = ctx + "/cmslogin.page"; |
|
159 |
+ } else { |
|
160 |
+ window.location = ctx + "/login.page"; |
|
121 | 161 |
} |
122 | 162 |
} |
123 | 163 |
}, |
... | ... | @@ -137,4 +177,4 @@ |
137 | 177 |
commit("setStoreReset"); |
138 | 178 |
}, |
139 | 179 |
}, |
140 |
-}); |
|
180 |
+});(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
... | ... | @@ -64,16 +64,38 @@ |
64 | 64 |
<p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p> |
65 | 65 |
<p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p> |
66 | 66 |
</div> |
67 |
- </div> |
|
67 |
+ <!-- <div class="oauth-login-section" v-if="!isAdminPage"> |
|
68 |
+ <div class="oauth-divider"> |
|
69 |
+ <span>또는</span> |
|
70 |
+ </div> |
|
71 |
+ |
|
72 |
+ <div class="oauth-buttons"> |
|
73 |
+ <button class="oauth-btn kakao-btn" @click="fnOAuthLogin('kakao')"> |
|
74 |
+ <img src="/images/kakao-icon.png" alt="카카오" class="oauth-icon" /> |
|
75 |
+ 카카오로 로그인 |
|
76 |
+ </button> |
|
77 |
+ |
|
78 |
+ <button class="oauth-btn naver-btn" @click="fnOAuthLogin('naver')"> |
|
79 |
+ <img src="/images/naver-icon.png" alt="네이버" class="oauth-icon" /> |
|
80 |
+ 네이버로 로그인 |
|
81 |
+ </button> |
|
82 |
+ |
|
83 |
+ <button class="oauth-btn google-btn" @click="fnOAuthLogin('google')"> |
|
84 |
+ <img src="/images/google-icon.png" alt="구글" class="oauth-icon" /> |
|
85 |
+ 구글로 로그인 |
|
86 |
+ </button> |
|
87 |
+ </div> |
|
88 |
+ </div> --> |
|
68 | 89 |
</div> |
69 | 90 |
</div> |
91 |
+ </div> |
|
70 | 92 |
</div> |
71 | 93 |
</template> |
72 | 94 |
|
73 | 95 |
<script> |
74 | 96 |
import { useStore } from "vuex"; |
75 | 97 |
import store from "../AppStore"; |
76 |
-import { loginProc } from "../../../resources/api/login"; |
|
98 |
+import { loginProc, oauthLogin, getOAuthUserInfo } from "../../../resources/api/login"; |
|
77 | 99 |
import queryParams from "../../../resources/js/queryParams"; |
78 | 100 |
|
79 | 101 |
export default { |
... | ... | @@ -99,74 +121,114 @@ |
99 | 121 |
this.isAdminPage = false; |
100 | 122 |
} |
101 | 123 |
}, |
124 |
+ |
|
125 |
+ // OAuth2 로그인 처리 (API 모듈 사용) |
|
126 |
+ fnOAuthLogin(provider) { |
|
127 |
+ const redirectUrl = this.restoreRedirect("redirect") || this.$route.fullPath; |
|
128 |
+ sessionStorage.setItem('oauth_redirect', redirectUrl); |
|
129 |
+ |
|
130 |
+ oauthLogin(provider); |
|
131 |
+ }, |
|
102 | 132 |
async fnLogin() { |
103 | 133 |
try { |
104 | 134 |
const res = await loginProc(this.member); |
105 | 135 |
if (res.status == 200) { |
106 | 136 |
const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분 |
137 |
+ |
|
107 | 138 |
if (loginType === 'J') { |
108 |
- // JWT 방식 |
|
109 |
- store.commit("setAuthorization", res.headers.authorization); |
|
110 |
- store.commit("setLoginMode", "J"); |
|
111 |
- localStorage.setItem("loginMode", "J"); |
|
112 |
- const base64String = store.state.authorization.split(".")[1]; |
|
113 |
- const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/"); |
|
114 |
- const jsonPayload = decodeURIComponent( |
|
115 |
- atob(base64).split("").map((c) => { |
|
116 |
- return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
|
117 |
- }).join("") |
|
118 |
- ); |
|
119 |
- const mbr = JSON.parse(jsonPayload); |
|
120 |
- store.commit("setMbrId", mbr.mbrId); |
|
121 |
- store.commit("setMbrNm", mbr.mbrNm); |
|
122 |
- store.commit("setRoles", mbr.roles); |
|
123 |
- } else if (loginType === 'S') { |
|
124 |
- store.commit("setLoginMode", "S"); |
|
125 |
- localStorage.setItem("loginMode", "S"); |
|
126 |
- const mbr = res.data; |
|
127 |
- store.commit("setAuthorization", null); |
|
128 |
- store.commit("setMbrId", mbr.mbrId); |
|
129 |
- store.commit("setMbrNm", mbr.mbrNm); |
|
130 |
- const roles = mbr.roles.map(r => ({ authority: r.authrtCd })); |
|
131 |
- store.commit("setRoles", roles); |
|
132 |
- } else { |
|
133 |
- alert("알 수 없는 로그인 방식입니다."); |
|
134 |
- return; |
|
135 |
- } |
|
136 |
- const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN"); |
|
137 |
- let url = this.restoreRedirect("redirect"); |
|
138 |
- if (url != null && url != "") { |
|
139 |
- const ctx = store.state.contextPath; |
|
140 |
- if (ctx !== "") { |
|
141 |
- // redirect 값에서 Context Path 추가 |
|
142 |
- url = this.$filters.ctxPath(url); |
|
143 |
- } else { |
|
144 |
- // redirect 값에서 기존 Context Path 제거 |
|
145 |
- url = url.replace(/^\/[^\/]+/, ""); // 첫 번째 '/' 이후의 경로만 남김 |
|
146 |
- } |
|
147 |
- const routeExists = this.$router.getRoutes().some(route => route.path === url); |
|
139 |
+ // JWT 방식 |
|
140 |
+ const token = res.headers.authorization; |
|
141 |
+ store.commit("setAuthorization", token); |
|
142 |
+ store.commit("setLoginMode", "J"); |
|
148 | 143 |
|
149 |
- if (url == this.$filters.ctxPath("/searchId.page") || url == this.$filters.ctxPath("/resetPswd.page")) { |
|
150 |
- this.$router.push({ path: this.$filters.ctxPath("/main.page") }); |
|
151 |
- } else if (routeExists) { |
|
152 |
- this.$router.push({ path: url }); |
|
153 |
- } else { |
|
154 |
- this.$router.push({ |
|
155 |
- path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/") |
|
156 |
- }); |
|
157 |
- } |
|
144 |
+ // localStorage에도 저장 |
|
145 |
+ localStorage.setItem("authorization", token); |
|
146 |
+ localStorage.setItem("loginMode", "J"); |
|
147 |
+ |
|
148 |
+ const base64String = token.split(".")[1]; |
|
149 |
+ const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/"); |
|
150 |
+ const jsonPayload = decodeURIComponent( |
|
151 |
+ atob(base64).split("").map((c) => { |
|
152 |
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
|
153 |
+ }).join("") |
|
154 |
+ ); |
|
155 |
+ const mbr = JSON.parse(jsonPayload); |
|
156 |
+ |
|
157 |
+ store.commit("setMbrId", mbr.mbrId); |
|
158 |
+ store.commit("setMbrNm", mbr.mbrNm); |
|
159 |
+ store.commit("setRoles", mbr.roles); |
|
160 |
+ |
|
161 |
+ // localStorage에 사용자 정보도 저장 |
|
162 |
+ localStorage.setItem("mbrId", mbr.mbrId); |
|
163 |
+ localStorage.setItem("mbrNm", mbr.mbrNm); |
|
164 |
+ localStorage.setItem("roles", JSON.stringify(mbr.roles)); |
|
165 |
+ |
|
166 |
+ } else if (loginType === 'S') { |
|
167 |
+ store.commit("setLoginMode", "S"); |
|
168 |
+ localStorage.setItem("loginMode", "S"); |
|
169 |
+ const mbr = res.data; |
|
170 |
+ store.commit("setAuthorization", null); |
|
171 |
+ store.commit("setMbrId", mbr.mbrId); |
|
172 |
+ store.commit("setMbrNm", mbr.mbrNm); |
|
173 |
+ const roles = mbr.roles.map(r => ({ authority: r.authrtCd })); |
|
174 |
+ store.commit("setRoles", roles); |
|
175 |
+ |
|
176 |
+ // localStorage에 세션 정보도 저장 |
|
177 |
+ localStorage.setItem("mbrId", mbr.mbrId); |
|
178 |
+ localStorage.setItem("mbrNm", mbr.mbrNm); |
|
179 |
+ localStorage.setItem("roles", JSON.stringify(roles)); |
|
180 |
+ |
|
158 | 181 |
} else { |
159 |
- this.$router.push({ |
|
160 |
- path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/") |
|
161 |
- }); |
|
182 |
+ alert("알 수 없는 로그인 방식입니다."); |
|
183 |
+ return; |
|
162 | 184 |
} |
163 | 185 |
|
164 |
- |
|
186 |
+ // 페이지 이동 처리 |
|
187 |
+ await this.handleLoginSuccess(); |
|
165 | 188 |
} |
166 | 189 |
} catch (error) { |
167 |
- alert(error.response.data.message); |
|
190 |
+ console.error("로그인 에러:", error); |
|
191 |
+ alert(error.response?.data?.message || "로그인에 실패했습니다."); |
|
168 | 192 |
} |
169 | 193 |
}, |
194 |
+ |
|
195 |
+ // 로그인 성공 후 페이지 이동 처리 |
|
196 |
+ async handleLoginSuccess() { |
|
197 |
+ const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN"); |
|
198 |
+ let redirectUrl = this.restoreRedirect("redirect"); |
|
199 |
+ |
|
200 |
+ if (redirectUrl && redirectUrl !== "") { |
|
201 |
+ // 특정 페이지들은 메인으로 리다이렉트 |
|
202 |
+ if (redirectUrl.includes("/searchId.page") || redirectUrl.includes("/resetPswd.page") || redirectUrl.includes("/login.page")) { |
|
203 |
+ redirectUrl = this.$filters.ctxPath("/"); |
|
204 |
+ } |
|
205 |
+ |
|
206 |
+ // Context Path 처리 |
|
207 |
+ if (!redirectUrl.startsWith(store.state.contextPath) && store.state.contextPath) { |
|
208 |
+ redirectUrl = this.$filters.ctxPath(redirectUrl); |
|
209 |
+ } |
|
210 |
+ |
|
211 |
+ |
|
212 |
+ // 라우터에 해당 경로가 존재하는지 확인 |
|
213 |
+ const routeExists = this.$router.getRoutes().some(route => route.path === redirectUrl); |
|
214 |
+ |
|
215 |
+ if (routeExists) { |
|
216 |
+ await this.$nextTick(); |
|
217 |
+ this.$router.push({ path: redirectUrl }); |
|
218 |
+ } else { |
|
219 |
+ const defaultPath = isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/"); |
|
220 |
+ this.$router.push({ path: defaultPath }); |
|
221 |
+ } |
|
222 |
+ } else { |
|
223 |
+ const defaultPath = isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/"); |
|
224 |
+ await this.$nextTick(); |
|
225 |
+ this.$router.push({ path: defaultPath }); |
|
226 |
+ } |
|
227 |
+ |
|
228 |
+ // redirect 세션 정리 |
|
229 |
+ sessionStorage.removeItem("redirect"); |
|
230 |
+ }, |
|
231 |
+ |
|
170 | 232 |
moveSearchId() { |
171 | 233 |
this.$router.push({ |
172 | 234 |
path: this.$filters.ctxPath("/resetPswd.page"), |
... | ... | @@ -183,13 +245,86 @@ |
183 | 245 |
}, |
184 | 246 |
}); |
185 | 247 |
}, |
248 |
+ |
|
249 |
+ // OAuth2 로그인 성공 후 처리 |
|
250 |
+async handleOAuthCallback() { |
|
251 |
+ const urlParams = new URLSearchParams(window.location.search); |
|
252 |
+ const error = urlParams.get('error'); |
|
253 |
+ |
|
254 |
+ if (error) { |
|
255 |
+ alert('소셜 로그인에 실패했습니다: ' + decodeURIComponent(urlParams.get('message') || error)); |
|
256 |
+ return; |
|
257 |
+ } |
|
258 |
+ |
|
259 |
+ // URL에 OAuth 관련 파라미터가 있는 경우 |
|
260 |
+ if (this.$route.query.oauth_success) { |
|
261 |
+ try { |
|
262 |
+ console.log("OAuth2 로그인 성공! 간단한 세션 처리를 시작합니다."); |
|
263 |
+ |
|
264 |
+ // OAuth는 일반적으로 세션 방식이므로 세션으로 설정 |
|
265 |
+ store.commit("setLoginMode", "S"); |
|
266 |
+ localStorage.setItem("loginMode", "S"); |
|
267 |
+ |
|
268 |
+ // 기본 사용자 정보 설정 - 서버에서 세션에 저장된 정보 사용 |
|
269 |
+ // 실제 사용자 정보는 다른 API나 페이지 로드 시 가져올 수 있음 |
|
270 |
+ const defaultMbrId = 'oauth_user_' + Date.now(); // 임시 ID |
|
271 |
+ const defaultMbrNm = 'OAuth 사용자'; |
|
272 |
+ const defaultRoles = [{ authority: 'ROLE_USER' }]; |
|
273 |
+ |
|
274 |
+ store.commit("setAuthorization", null); |
|
275 |
+ store.commit("setMbrId", defaultMbrId); |
|
276 |
+ store.commit("setMbrNm", defaultMbrNm); |
|
277 |
+ store.commit("setRoles", defaultRoles); |
|
278 |
+ |
|
279 |
+ // localStorage에 정보 저장 |
|
280 |
+ localStorage.setItem("mbrId", defaultMbrId); |
|
281 |
+ localStorage.setItem("mbrNm", defaultMbrNm); |
|
282 |
+ localStorage.setItem("roles", JSON.stringify(defaultRoles)); |
|
283 |
+ |
|
284 |
+ console.log("OAuth 세션 로그인 완료 - Store 상태:", { |
|
285 |
+ authorization: store.state.authorization, |
|
286 |
+ mbrId: store.state.mbrId, |
|
287 |
+ mbrNm: store.state.mbrNm, |
|
288 |
+ roles: store.state.roles, |
|
289 |
+ loginMode: store.state.loginMode |
|
290 |
+ }); |
|
291 |
+ |
|
292 |
+ // 페이지 이동 처리 |
|
293 |
+ await this.$nextTick(); |
|
294 |
+ |
|
295 |
+ // 리다이렉트 URL 처리 |
|
296 |
+ const redirectUrl = sessionStorage.getItem('oauth_redirect'); |
|
297 |
+ sessionStorage.removeItem('oauth_redirect'); |
|
298 |
+ |
|
299 |
+ if (redirectUrl && redirectUrl !== '/login' && !redirectUrl.includes('/login.page')) { |
|
300 |
+ console.log("OAuth 리다이렉트:", redirectUrl); |
|
301 |
+ this.$router.replace({ path: redirectUrl }); |
|
302 |
+ } else { |
|
303 |
+ // 메인 페이지로 이동 |
|
304 |
+ const mainPath = this.$filters.ctxPath("/"); |
|
305 |
+ console.log("OAuth 메인 페이지로 이동:", mainPath); |
|
306 |
+ this.$router.replace({ path: mainPath }); |
|
307 |
+ } |
|
308 |
+ |
|
309 |
+ } catch (error) { |
|
310 |
+ console.error('OAuth 콜백 처리 실패:', error); |
|
311 |
+ alert('OAuth 로그인 처리 중 오류가 발생했습니다: ' + (error.message || '알 수 없는 오류')); |
|
312 |
+ |
|
313 |
+ // 에러 발생 시 로그인 페이지로 이동 |
|
314 |
+ store.commit("setStoreReset"); |
|
315 |
+ this.$router.push({ path: this.$filters.ctxPath("/login.page") }); |
|
316 |
+ } |
|
317 |
+ } |
|
318 |
+} |
|
186 | 319 |
}, |
187 | 320 |
watch: {}, |
188 | 321 |
computed: {}, |
189 | 322 |
components: {}, |
190 | 323 |
created() { |
191 | 324 |
this.checkAdminPage(); |
325 |
+ // OAuth2 콜백 처리 |
|
326 |
+ this.handleOAuthCallback(); |
|
192 | 327 |
}, |
193 | 328 |
mounted() {}, |
194 | 329 |
}; |
195 |
-</script> |
|
330 |
+</script>(파일 끝에 줄바꿈 문자 없음) |
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?