From d40258a95bdfd599de76c30e7464c7799355668b Mon Sep 17 00:00:00 2001 From: hoon7566 Date: Thu, 3 Mar 2022 20:29:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(customer=20vue,=20user-service):=20custome?= =?UTF-8?q?r=20vue=20OAuth=20=EC=9D=B8=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - customer vue 인증페이지 구현 - customer OAuth 로그인 구현 - user-service success handling 구현 --- customer-apigateway-service/build.gradle | 2 + .../filter/AuthorizationHeaderFilter.java | 11 +- .../filter/GlobalFilter.java | 53 ++++++++ .../handler/GlobalExceptionHandler.java | 64 ++++++++++ .../security/JwtTokenProvider.java | 21 +-- .../src/main/resources/application.yml | 27 ++++ customer-vue/.env | 3 + customer-vue/package.json | 2 + customer-vue/src/api/auth.js | 26 ++++ customer-vue/src/api/store.js | 34 +++++ customer-vue/src/api/user.js | 32 +++++ customer-vue/src/assets/justLogo.png | Bin 0 -> 3767 bytes customer-vue/src/assets/logo_google.png | Bin 0 -> 3367 bytes customer-vue/src/assets/logo_naver.svg | 19 +++ customer-vue/src/common/jwt.js | 38 ++++++ customer-vue/src/main.js | 29 +++++ customer-vue/src/router/router.js | 29 ++++- customer-vue/src/views/Layout/AuthSuccess.vue | 20 +++ customer-vue/src/views/LoginPage.vue | 120 ++++++++++++++++++ customer-vue/vue.config.js | 5 +- .../filter/GlobalFilter.java | 1 - .../domain/jwt/service/OAuthService.java | 29 ++++- .../global/security/SecurityConfig.java | 4 +- 23 files changed, 538 insertions(+), 31 deletions(-) create mode 100644 customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/GlobalFilter.java create mode 100644 customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/handler/GlobalExceptionHandler.java create mode 100644 customer-vue/.env create mode 100644 customer-vue/src/api/auth.js create mode 100644 customer-vue/src/api/store.js create mode 100644 customer-vue/src/api/user.js create mode 100644 customer-vue/src/assets/justLogo.png create mode 100644 customer-vue/src/assets/logo_google.png create mode 100644 customer-vue/src/assets/logo_naver.svg create mode 100644 customer-vue/src/common/jwt.js create mode 100644 customer-vue/src/views/Layout/AuthSuccess.vue create mode 100644 customer-vue/src/views/LoginPage.vue diff --git a/customer-apigateway-service/build.gradle b/customer-apigateway-service/build.gradle index 5094502..b8cc022 100644 --- a/customer-apigateway-service/build.gradle +++ b/customer-apigateway-service/build.gradle @@ -31,6 +31,8 @@ dependencies { // https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' diff --git a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java index a8af87b..017c185 100644 --- a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java +++ b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java @@ -1,18 +1,25 @@ package com.justpickup.customerapigatewayservice.filter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.justpickup.customerapigatewayservice.security.JwtTokenProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.HashMap; +import java.util.Map; + @Component @Slf4j public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory { @@ -44,9 +51,7 @@ public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory { + + public GlobalFilter(){ + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); // reactive포함된거로 import + ServerHttpResponse response = exchange.getResponse(); + + log.info("Global com.example.scg.filter baseMessgae: {}", config.getBaseMessage()); + + // Global pre Filter + if (config.isPreLogger()){ + log.info("Global Filter Start: request id -> {}" , request.getId()); + log.info("Global Filter Start: request path -> {}" , request.getPath()); + } + + // Global Post Filter + //Mono는 webflux에서 단일값 전송할때 Mono값으로 전송 + return chain.filter(exchange).then(Mono.fromRunnable(()->{ + + if (config.isPostLogger()){ + log.info("Global Filter End: response statuscode -> {}" , response.getStatusCode()); + } + })); + + }; + } + + @Data + public static class Config { + private String baseMessage; + private boolean preLogger; + private boolean postLogger; + } +} diff --git a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/handler/GlobalExceptionHandler.java b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..b40813e --- /dev/null +++ b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/handler/GlobalExceptionHandler.java @@ -0,0 +1,64 @@ +package com.justpickup.customerapigatewayservice.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GlobalExceptionHandler implements ErrorWebExceptionHandler { + + @Autowired + private ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + List> jwtExceptions = + List.of(SignatureException.class, + MalformedJwtException.class, + UnsupportedJwtException.class, + IllegalArgumentException.class); + Class exceptionClass = ex.getClass(); + + Map responseBody = new HashMap<>(); + if (exceptionClass == ExpiredJwtException.class) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + responseBody.put("code", "EXPIRED"); + responseBody.put("message", "Access Token is Expired!"); + } else if (jwtExceptions.contains(exceptionClass)){ + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + responseBody.put("code", "INVALID"); + responseBody.put("message", "Invalid Access Token"); + }else{ + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + responseBody.put("code", "INVALID"); + } + + DataBuffer wrap = null; + try { + byte[] bytes = objectMapper.writeValueAsBytes(responseBody); + wrap = exchange.getResponse().bufferFactory().wrap(bytes); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + return exchange.getResponse().writeWith(Flux.just(wrap)); + } + +} diff --git a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java index af5fc77..99404d9 100644 --- a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java +++ b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java @@ -73,25 +73,12 @@ public class JwtTokenProvider { return (List) getClaimsFromJwtToken(token).get("roles"); } - public boolean validateJwtToken(String token) { + public void validateJwtToken(String token) { try { Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); - return true; - } catch (SignatureException e) { - log.error("Invalid JWT signature: {}", e.getMessage()); - return false; - } catch (MalformedJwtException e) { - log.error("Invalid JWT token: {}", e.getMessage()); - return false; - } catch (ExpiredJwtException e) { - log.error("JWT token is expired: {}", e.getMessage()); - return false; - } catch (UnsupportedJwtException e) { - log.error("JWT token is unsupported: {}", e.getMessage()); - return false; - } catch (IllegalArgumentException e) { - log.error("JWT claims string is empty: {}", e.getMessage()); - return false; + } catch (SignatureException | MalformedJwtException | + UnsupportedJwtException | IllegalArgumentException | ExpiredJwtException jwtException) { + throw jwtException; } } diff --git a/customer-apigateway-service/src/main/resources/application.yml b/customer-apigateway-service/src/main/resources/application.yml index 31f9c15..f42e06c 100644 --- a/customer-apigateway-service/src/main/resources/application.yml +++ b/customer-apigateway-service/src/main/resources/application.yml @@ -15,6 +15,25 @@ spring: cloud: gateway: + default-filters: + - name: GlobalFilter + args: + baseMessage: Spring Cloud Gateway Global Filter + preLogger: true + postLogger: true + globalcors: + cors-configurations: + '[/**]': + allowedOrigins: "http://just-pickup.com:8080" + allowedMethods: + - GET + - POST + - DELETE + - PUT + - OPTIONS + - DELETE + allowedHeaders: '*' + allow-credentials: true routes: - id: owner-frontend-service uri: lb://CUSTOMER-FRONTEND-SERVICE @@ -33,6 +52,7 @@ spring: predicates: - Path=/store-service/** filters: + - AuthorizationHeaderFilter - RewritePath=/store-service/(?.*),/$\{segment} - id: user-service uri: lb://USER-SERVICE @@ -55,6 +75,13 @@ spring: - Method=POST filters: - RewritePath=/user-service/(?.*),/$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/auth/reissue + - Method=GET + filters: + - RewritePath=/user-service/(?.*),/$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: diff --git a/customer-vue/.env b/customer-vue/.env new file mode 100644 index 0000000..999a455 --- /dev/null +++ b/customer-vue/.env @@ -0,0 +1,3 @@ +VUE_APP_BASEURL=http://just-pickup.com:8080 +VUE_APP_OWNER_SERVICE_BASEURL=http://just-pickup.com:8001 +VUE_APP_CUSTOMER_SERVICE_BASEURL=http://just-pickup.com:8000 \ No newline at end of file diff --git a/customer-vue/package.json b/customer-vue/package.json index 5dba678..27c8031 100644 --- a/customer-vue/package.json +++ b/customer-vue/package.json @@ -19,6 +19,8 @@ "@vue/cli-plugin-babel": "^5.0.0", "@vue/cli-plugin-eslint": "^5.0.0", "@vue/cli-service": "^5.0.0", + "axios": "^0.26.0", + "moment": "^2.29.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3", "vue-template-compiler": "^2.6.14" diff --git a/customer-vue/src/api/auth.js b/customer-vue/src/api/auth.js new file mode 100644 index 0000000..2c51de7 --- /dev/null +++ b/customer-vue/src/api/auth.js @@ -0,0 +1,26 @@ +import axios from "axios"; +import jwt from "@/common/jwt"; + +export default { + async requestReissue() { + const config = { + headers: { + "X-AUTH-TOKEN": jwt.getToken() + } + } + + const res = await axios.get(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+"/user-service/auth/reissue", config); + + const accessToken = res.data.data.accessToken; + jwt.saveToken(accessToken); + jwt.saveExpiredTime(res.data.data.expiredTime); + axios.defaults.headers.common['Authorization'] = "Bearer " + accessToken; + + return accessToken; + }, + requestCheckAccessToken() { + axios.defaults.headers.common['Authorization'] = "Bearer " + jwt.getToken(); + + return axios.get(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+"/user-service/auth/check/access-token"); + } +} \ No newline at end of file diff --git a/customer-vue/src/api/store.js b/customer-vue/src/api/store.js new file mode 100644 index 0000000..37179da --- /dev/null +++ b/customer-vue/src/api/store.js @@ -0,0 +1,34 @@ +import axios from "axios"; + +export default { + getCategoryList(){ + return axios.get(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+'/store-service/category/'); + }, + putCategoryList(data){ + return axios({ + method:'put', + url:process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+'/store-service/category', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + }, + data: data, + responseType:'json' + }) + }, + getItemById(itemId){ + return axios.get(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+'/store-service/item/'+itemId) + }, + saveItem(method, itemData){ + return axios({ + method:method, + url: process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+'/store-service/item', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + }, + data: itemData, + responseType:'json' + }) + }, +} \ No newline at end of file diff --git a/customer-vue/src/api/user.js b/customer-vue/src/api/user.js new file mode 100644 index 0000000..7eb0b07 --- /dev/null +++ b/customer-vue/src/api/user.js @@ -0,0 +1,32 @@ +import axios from "axios"; +import jwt from '../common/jwt.js'; + +export default { + requestRegisterUser(user) { + return axios.post(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+"/user-service/store-owner", user); + }, + + async requestLoginUser(email, password) { + const user = { + email: email, + password: password + } + + try { + const response = await axios.post(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+"/user-service/login", user); + const data = response.data.data; + + jwt.saveToken(data.accessToken); + jwt.saveExpiredTime(data.expiredTime); + + axios.defaults.headers.common['Authorization'] = "Bearer " + data.accessToken; + + return true; + } catch (err) { + console.log("Error = ", err); + return false; + } + + } +} + diff --git a/customer-vue/src/assets/justLogo.png b/customer-vue/src/assets/justLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..538e028ec0986972f1eb24b4cdbc128c26df73a2 GIT binary patch literal 3767 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 E0ff{r;s5{u literal 0 HcmV?d00001 diff --git a/customer-vue/src/assets/logo_google.png b/customer-vue/src/assets/logo_google.png new file mode 100644 index 0000000000000000000000000000000000000000..919dd474c7cefdfa5e6f0ad4c6904b9530856083 GIT binary patch literal 3367 zcmV+?4cPLDP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0006~Nkl-_ddV- zJzh4LVnb_ed%7)q?)rg8tQL>PEl5VSqw}7nnhW8XE~JosPl@Nvp>i_KJdA? z=xWgVa#)m=6#pr`hn<3J6>s;)2B$xj6t)y^Zw98z0ko73uj1~QYE(nto-k5wYVFU2 zfQK8fW`W}lz-K5fx;pZ67G@7nw*j!uIA!zHH!T}5Z&=qPN2>Q)_*Q`5rYmjX!2wJH zrvcs+c7N#0wsgVJ9iwwR<{bvAO?_M?fhY0#djKxNkDd0DV9_5s9Y) zQdj`|z%^&)*eUn?rTwV(wd5I(c|&VyqnU?*GwX!IR_*sGO>6XJ8(q%WeUI#U;-Efh7*yvY7U3L0|0Vz(R1w^_+0=1002ovPDHLkV1g_cK~Vqz literal 0 HcmV?d00001 diff --git a/customer-vue/src/assets/logo_naver.svg b/customer-vue/src/assets/logo_naver.svg new file mode 100644 index 0000000..ca32db0 --- /dev/null +++ b/customer-vue/src/assets/logo_naver.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/customer-vue/src/common/jwt.js b/customer-vue/src/common/jwt.js new file mode 100644 index 0000000..0783eb5 --- /dev/null +++ b/customer-vue/src/common/jwt.js @@ -0,0 +1,38 @@ + +const moment = require('moment'); +const ACCESS_TOKEN_NAME = "accessToken"; +const EXPIRED_TIME_NAME = "expiredTime"; + +const tag = "[jwt]"; + +export default { + getToken() { + return localStorage.getItem(ACCESS_TOKEN_NAME); + }, + saveToken(token) { + localStorage.setItem(ACCESS_TOKEN_NAME, token); + }, + getExpiredTime() { + return localStorage.getItem(EXPIRED_TIME_NAME); + }, + saveExpiredTime(expiredTime) { + localStorage.setItem(EXPIRED_TIME_NAME, expiredTime); + }, + destroyAll() { + localStorage.removeItem(ACCESS_TOKEN_NAME); + localStorage.removeItem(EXPIRED_TIME_NAME); + }, + isExpired() { + const expiredTime = this.getExpiredTime(); + + const expiredMoment = moment(expiredTime); + let currentMoment = moment(); + + const difference = moment.duration(expiredMoment.diff(currentMoment)).asSeconds(); + + console.log(tag, "expireMoment = ", expiredMoment, "currentMoment = ", currentMoment, "diff = ", difference); + + // 만료 30초 전일 경우 만료로 판단 + return difference <= 30; + } +} \ No newline at end of file diff --git a/customer-vue/src/main.js b/customer-vue/src/main.js index 5e20f20..06d671f 100644 --- a/customer-vue/src/main.js +++ b/customer-vue/src/main.js @@ -2,7 +2,10 @@ import Vue from 'vue'; import App from './App.vue'; import vuetify from "@/plugins/vuetify"; import router from "./router/router.js"; +import axios from "axios"; +import auth from "@/api/auth"; +axios.defaults.withCredentials = true; Vue.config.productionTip = false new Vue({ @@ -10,3 +13,29 @@ new Vue({ router, render: h => h(App), }).$mount('#app') + +axios.interceptors.response.use( + (response) => { + return response; + }, + async (error) => { + const originalRequest = error.config; + if (error.response.status === 401) { + let code = error.response.data.code; + if (code === "EXPIRED") { + console.log("## expired"); + try { + const accessToken = await auth.requestReissue(); + originalRequest.headers.Authorization = "Bearer " + accessToken; + return axios(originalRequest); + } catch (reissueError) { + window.location.href = process.env.VUE_APP_BASEURL+"/login"; + alert("권한이 없습니다. 다시 로그인 해주세요"); + } + } + window.location.href = process.env.VUE_APP_BASEURL+"/login"; + alert("권한이 없습니다. 다시 로그인해주세요."); + } + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/customer-vue/src/router/router.js b/customer-vue/src/router/router.js index c621f62..d8d6c83 100644 --- a/customer-vue/src/router/router.js +++ b/customer-vue/src/router/router.js @@ -2,9 +2,17 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import HomeLayout from '../views/Layout/HomeLayout.vue'; +const ACCESS_TOKEN_NAME = "accessToken"; +const EXPIRED_TIME_NAME = "expiredTime"; Vue.use(VueRouter); +const auth = async function (to, from, next) { + localStorage.setItem(ACCESS_TOKEN_NAME, to.query.accessToken); + localStorage.setItem(EXPIRED_TIME_NAME, to.query.expiredTime) + next(); +}; + const routes = [ { path: '/', @@ -17,7 +25,26 @@ const routes = [ component: () => import('../views/HomeView') } ] - } + }, + { + path: '/login', + redirect: 'login', + component: HomeLayout, + children: [ + { + path: "/login", + name: 'login', + component: () => import('../views/LoginPage') + } + ] + }, + { + path: '/auth', + name: 'auth', + beforeEnter: auth, + component: () => import('../views/Layout/AuthSuccess.vue') + + }, ] const router = new VueRouter({ diff --git a/customer-vue/src/views/Layout/AuthSuccess.vue b/customer-vue/src/views/Layout/AuthSuccess.vue new file mode 100644 index 0000000..579fe10 --- /dev/null +++ b/customer-vue/src/views/Layout/AuthSuccess.vue @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/customer-vue/src/views/LoginPage.vue b/customer-vue/src/views/LoginPage.vue new file mode 100644 index 0000000..48e640d --- /dev/null +++ b/customer-vue/src/views/LoginPage.vue @@ -0,0 +1,120 @@ + + + + + \ No newline at end of file diff --git a/customer-vue/vue.config.js b/customer-vue/vue.config.js index 5b768af..99fd690 100644 --- a/customer-vue/vue.config.js +++ b/customer-vue/vue.config.js @@ -1,3 +1,6 @@ module.exports = { - transpileDependencies: true + transpileDependencies: true, + devServer:{ + allowedHosts: 'all', + } } diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/GlobalFilter.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/GlobalFilter.java index a34d533..e052f7a 100644 --- a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/GlobalFilter.java +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/GlobalFilter.java @@ -13,7 +13,6 @@ import reactor.core.publisher.Mono; @Slf4j public class GlobalFilter extends AbstractGatewayFilterFactory { - private static final String TEST_CIRCUIT_BREAKER = "testCircuitBreaker"; public GlobalFilter(){ super(Config.class); diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java index 8fad368..ce8ffde 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java @@ -22,6 +22,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -75,10 +76,20 @@ public class OAuthService implements OAuth2UserService