From 731e79ece6a0a0cb625ea6351c16800d7c2890e9 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Fri, 25 Feb 2022 12:37:00 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat(owner-vue):=20=EC=A0=90=EC=A3=BC?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20-=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4,=20=ED=97=A4=EB=8D=94=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일, 패스워드 유효성 검사 - 로그인 성공 시 default header 설정 --- owner-vue/src/api/user.js | 32 +++++++++++++--- owner-vue/src/views/LoginUser.vue | 61 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 owner-vue/src/views/LoginUser.vue diff --git a/owner-vue/src/api/user.js b/owner-vue/src/api/user.js index 50ad23c..0e1fff5 100644 --- a/owner-vue/src/api/user.js +++ b/owner-vue/src/api/user.js @@ -1,8 +1,30 @@ -import axios from "axios"; - export default { + requestRegisterUser(user) { + return axios.post("http://localhost:8001/user-service/store-owner", user); + }, - requestRegisterUser(user) { - return axios.post("http://localhost:8001/user-service/store-owner", user); + async requestLoginUser(email, password) { + const user = { + email: email, + password: password } -} \ No newline at end of file + + try { + const response = await axios.post("http://localhost:8001/user-service/login", user); + console.log(response); + const AUTH_TOKEN = response.data.data.access_token; + localStorage.setItem('access_token', AUTH_TOKEN); + axios.defaults.headers.common['Authorization'] = AUTH_TOKEN; + + return true; + } catch (err) { + console.log("Error = ", err); + alert("로그인 실패!"); + + return false; + } + + } +} + +import axios from "axios"; diff --git a/owner-vue/src/views/LoginUser.vue b/owner-vue/src/views/LoginUser.vue new file mode 100644 index 0000000..2496b06 --- /dev/null +++ b/owner-vue/src/views/LoginUser.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file From 516c192211df6c0d983d5dac604b7bf343103d18 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Mon, 28 Feb 2022 16:47:04 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(owner-vue):=20JWT=20refresh=20Token=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 이동 시 유효기간 판단 후 reissue 호출 로직 추가 - api 호출 시 response code에 따라 reissue 호출 로직 추가 (code => EXPIRED 일 경우) --- owner-vue/src/api/auth.js | 26 +++++++++++++++++++++ owner-vue/src/api/user.js | 12 ++++++---- owner-vue/src/common/jwt.js | 38 +++++++++++++++++++++++++++++++ owner-vue/src/main.js | 43 +++++++++++++++++++++++++++++++---- owner-vue/src/router/index.js | 19 +++++++++++++++- 5 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 owner-vue/src/api/auth.js create mode 100644 owner-vue/src/common/jwt.js diff --git a/owner-vue/src/api/auth.js b/owner-vue/src/api/auth.js new file mode 100644 index 0000000..32b2ef9 --- /dev/null +++ b/owner-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("http://localhost:8001/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("http://localhost:8001/user-service/auth/check/accessToken"); + } +} \ No newline at end of file diff --git a/owner-vue/src/api/user.js b/owner-vue/src/api/user.js index 0e1fff5..9a694ee 100644 --- a/owner-vue/src/api/user.js +++ b/owner-vue/src/api/user.js @@ -1,3 +1,5 @@ +import jwt from '../common/jwt.js'; + export default { requestRegisterUser(user) { return axios.post("http://localhost:8001/user-service/store-owner", user); @@ -11,10 +13,12 @@ export default { try { const response = await axios.post("http://localhost:8001/user-service/login", user); - console.log(response); - const AUTH_TOKEN = response.data.data.access_token; - localStorage.setItem('access_token', AUTH_TOKEN); - axios.defaults.headers.common['Authorization'] = AUTH_TOKEN; + 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) { diff --git a/owner-vue/src/common/jwt.js b/owner-vue/src/common/jwt.js new file mode 100644 index 0000000..0783eb5 --- /dev/null +++ b/owner-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/owner-vue/src/main.js b/owner-vue/src/main.js index c2d00de..d630ed4 100644 --- a/owner-vue/src/main.js +++ b/owner-vue/src/main.js @@ -1,7 +1,11 @@ -import Vue from 'vue' -import App from './App.vue' -import vuetify from './plugins/vuetify' -import router from './router' +import Vue from 'vue'; +import App from './App.vue'; +import vuetify from './plugins/vuetify'; +import router from './router'; +import axios from "axios"; +import auth from "./api/auth.js" + +axios.defaults.withCredentials = true; Vue.config.productionTip = false @@ -9,4 +13,33 @@ new Vue({ vuetify, router, render: h => h(App) -}).$mount('#app') \ No newline at end of file +}).$mount('#app') + +axios.interceptors.response.use( + (response) => { + return response; + }, + async (error) => { + try { + const originalRequest = error.config; + if (error.response.status === 401) { + // access token 만료 시 + if (error.response.data.code == "EXPIRED") { + const accessToken = await auth.requestReissue(); + + originalRequest.headers.Authorization = "Bearer " + accessToken; + return axios(originalRequest); + } + // 그외 에러일 시 + alert("로그인 정보가 일치하지 않습니다."); + await router.replace('/login'); + return; + } + } catch (error) { + alert("로그인 정보가 일치하지 않습니다."); + await router.replace('/login'); + return; + } + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/owner-vue/src/router/index.js b/owner-vue/src/router/index.js index 1fc109b..5d593a4 100644 --- a/owner-vue/src/router/index.js +++ b/owner-vue/src/router/index.js @@ -4,13 +4,30 @@ import VueRouter from 'vue-router' import DashboardLayout from "@/views/Layout/DashboardLayout"; import AuthLayout from "@/views/Layout/AuthLayout"; +import jwt from "../common/jwt.js"; +import auth from "../api/auth.js"; + Vue.use(VueRouter) +const authCheck = async function (to, from, next) { + try { + if (jwt.isExpired()) { + // refresh 호출 + await auth.requestReissue(); + } else { + await auth.requestCheckAccessToken(); + } + } catch (error) { + await router.replace("/login"); + } + next(); +}; const routes = [ { path: '/dashboard', redirect: 'dashboard', component: DashboardLayout, + beforeEnter: authCheck, children: [ { path: "/dashboard", @@ -64,4 +81,4 @@ const router = new VueRouter({ routes }) -export default router +export default router \ No newline at end of file From b6b3e393806f91094f9fffd6fc25ae0306da38a1 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Mon, 28 Feb 2022 16:50:22 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat(owner-apigateway-service):=20with=20cr?= =?UTF-8?q?edentials=20=EB=B0=8F=20exception=20handler=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - with credentials : true 옵션 추가 - 해당 옵션 추가로 인한 access-control-allow-origin : * 에서 vue 서버 아이피로 변경 - access token expired 일 경우 response body 추가 --- .../OwnerApigatewayServiceApplication.java | 8 ++++ .../handler/GlobalExceptionHandler.java | 46 +++++++++++++++++++ .../security/JwtTokenProvider.java | 2 +- .../src/main/resources/application.yml | 10 +++- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/handler/GlobalExceptionHandler.java diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/OwnerApigatewayServiceApplication.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/OwnerApigatewayServiceApplication.java index a739e2a..2bc155f 100644 --- a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/OwnerApigatewayServiceApplication.java +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/OwnerApigatewayServiceApplication.java @@ -1,8 +1,11 @@ package com.justpickup.ownerapigatewayservice; +import com.justpickup.ownerapigatewayservice.handler.GlobalExceptionHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.context.annotation.Bean; @SpringBootApplication @EnableEurekaClient @@ -12,4 +15,9 @@ public class OwnerApigatewayServiceApplication { SpringApplication.run(OwnerApigatewayServiceApplication.class, args); } + @Bean + public ErrorWebExceptionHandler globalExceptionHandler() { + return new GlobalExceptionHandler(); + } + } diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/handler/GlobalExceptionHandler.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..8c309cd --- /dev/null +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/handler/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.justpickup.ownerapigatewayservice.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +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.Map; + +public class GlobalExceptionHandler implements ErrorWebExceptionHandler { + + @Autowired + private ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + + 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!"); + } + + 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/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java index f5752f5..e4f4767 100644 --- a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java @@ -85,7 +85,7 @@ public class JwtTokenProvider { return false; } catch (ExpiredJwtException e) { log.error("JWT token is expired: {}", e.getMessage()); - return false; + throw e; } catch (UnsupportedJwtException e) { log.error("JWT token is unsupported: {}", e.getMessage()); return false; diff --git a/owner-apigateway-service/src/main/resources/application.yml b/owner-apigateway-service/src/main/resources/application.yml index a4635b5..5c7ae5e 100644 --- a/owner-apigateway-service/src/main/resources/application.yml +++ b/owner-apigateway-service/src/main/resources/application.yml @@ -17,7 +17,7 @@ spring: globalcors: cors-configurations: '[/**]': - allowedOrigins: "*" + allowedOrigins: "http://localhost:8080" allowedMethods: - GET - POST @@ -25,6 +25,7 @@ spring: - PUT - OPTIONS allowedHeaders: '*' + allow-credentials: true routes: - id: owner-frontend-service uri: lb://OWNER-FRONTEND-SERVICE @@ -32,18 +33,22 @@ spring: - Path=/owner-frontend-service/** filters: - RewritePath=/owner-frontend-service/(?.*),/$\{segment} + - id: order-service uri: lb://ORDER-SERVCIE predicates: - Path=/order-service/** filters: - RewritePath=/order-service/(?.*),/$\{segment} + - AuthorizationHeaderFilter + - id: store-service uri: lb://STORE-SERVCIE predicates: - Path=/store-service/** filters: - RewritePath=/store-service/(?.*),/$\{segment} + - id: user-service uri: lb://USER-SERVICE predicates: @@ -54,7 +59,7 @@ spring: - id: user-service uri: lb://USER-SERVICE predicates: - - Path=/user-service/refreshToken + - Path=/user-service/auth/reissue - Method=GET filters: - RewritePath=/user-service/(?.*),/$\{segment} @@ -78,6 +83,7 @@ spring: - Path=/user-service/** filters: - RewritePath=/user-service/(?.*),/$\{segment} + - AuthorizationHeaderFilter token: access-expired-time: 3600000 From 5f46d73a225ecda5e133f79fc79a320d64287aa3 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Mon, 28 Feb 2022 16:54:16 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(user-service):=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - access token 재발급 시 response body에 만료 시간 추가 - 페이지 이동 시 사용할 access token check 로직 추가 --- .../AccessTokenNotValidException.java | 2 +- .../jwt/service/AccessTokenService.java | 6 +++++ .../jwt/service/AccessTokenServiceImpl.java | 23 ++++++++++++++++ .../jwt/service/RefreshTokenServiceImpl.java | 3 +++ .../domain/jwt/web/AuthController.java | 27 ++++++++++++++----- .../domain/user/dto/JwtTokenDto.java | 6 ++++- .../exception/GlobalExceptionHandler.java | 2 +- .../security/LoginAuthenticationFilter.java | 16 +++++------ .../global/utils/JwtTokenProvider.java | 10 +++---- 9 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenService.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenServiceImpl.java diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/AccessTokenNotValidException.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/AccessTokenNotValidException.java index a0ffe60..e4fd941 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/AccessTokenNotValidException.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/AccessTokenNotValidException.java @@ -6,6 +6,6 @@ import org.springframework.http.HttpStatus; public class AccessTokenNotValidException extends CustomException { public AccessTokenNotValidException(String message) { - super(HttpStatus.FORBIDDEN, message); + super(HttpStatus.UNAUTHORIZED, message); } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenService.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenService.java new file mode 100644 index 0000000..9db3834 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenService.java @@ -0,0 +1,6 @@ +package com.justpickup.userservice.domain.jwt.service; + +public interface AccessTokenService { + + void checkAccessToken(String authorizationHeader); +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenServiceImpl.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenServiceImpl.java new file mode 100644 index 0000000..0c6cc0a --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/AccessTokenServiceImpl.java @@ -0,0 +1,23 @@ +package com.justpickup.userservice.domain.jwt.service; + +import com.justpickup.userservice.domain.jwt.exception.AccessTokenNotValidException; +import com.justpickup.userservice.global.utils.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AccessTokenServiceImpl implements AccessTokenService { + + private final JwtTokenProvider tokenProvider; + + @Override + public void checkAccessToken(String authorizationHeader) { + + String token = authorizationHeader.replace("Bearer", ""); + + if (!tokenProvider.validateJwtToken(token)) { + throw new AccessTokenNotValidException("Access Token is not Valid"); + } + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java index fcd9981..115a9ad 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java @@ -19,6 +19,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -69,9 +70,11 @@ public class RefreshTokenServiceImpl implements RefreshTokenService { .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/refreshToken", roles); + Date expiredTime = jwtTokenProvider.getExpiredTime(newAccessToken); return JwtTokenDto.builder() .accessToken(newAccessToken) + .accessTokenExpiredDate(expiredTime) .refreshToken(refreshToken) .build(); } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java index 39cfd79..f679a11 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java @@ -1,6 +1,7 @@ package com.justpickup.userservice.domain.jwt.web; -import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl; +import com.justpickup.userservice.domain.jwt.service.AccessTokenService; +import com.justpickup.userservice.domain.jwt.service.RefreshTokenService; import com.justpickup.userservice.domain.user.dto.JwtTokenDto; import com.justpickup.userservice.global.dto.Result; import com.justpickup.userservice.global.utils.CookieProvider; @@ -15,20 +16,22 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.ws.rs.core.HttpHeaders; +import java.text.SimpleDateFormat; @RestController @RequiredArgsConstructor +@RequestMapping("/auth") @Slf4j public class AuthController { - private final RefreshTokenServiceImpl refreshTokenServiceImpl; + private final RefreshTokenService refreshTokenService; + private final AccessTokenService accessTokenService; private final CookieProvider cookieProvider; - @GetMapping("/refreshToken") + @GetMapping("/reissue") public ResponseEntity refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken, @CookieValue("refresh-token") String refreshToken) { - - JwtTokenDto jwtTokenDto = refreshTokenServiceImpl.refreshJwtToken(accessToken, refreshToken); + JwtTokenDto jwtTokenDto = refreshTokenService.refreshJwtToken(accessToken, refreshToken); ResponseCookie responseCookie = cookieProvider.createRefreshTokenCookie(refreshToken); @@ -42,9 +45,12 @@ public class AuthController { @AllArgsConstructor static class RefreshTokenResponse { private String accessToken; + private String expiredTime; public RefreshTokenResponse(JwtTokenDto jwtTokenDto) { this.accessToken = jwtTokenDto.getAccessToken(); + this.expiredTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(jwtTokenDto.getAccessTokenExpiredDate()); } } @@ -52,7 +58,7 @@ public class AuthController { public ResponseEntity logout(@RequestHeader("X-AUTH-TOKEN") String accessToken, @RequestHeader("REFRESH-TOKEN") String refreshToken) { - refreshTokenServiceImpl.logoutToken(accessToken); + refreshTokenService.logoutToken(accessToken); ResponseCookie refreshCookie = cookieProvider.removeRefreshTokenCookie(); @@ -60,4 +66,13 @@ public class AuthController { .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) .body(Result.createErrorResult("")); } + + @GetMapping("/check/access-token") + public ResponseEntity checkAccessToken(@RequestHeader(name = "Authorization") String authorization) { + + accessTokenService.checkAccessToken(authorization); + + return ResponseEntity.status(HttpStatus.OK) + .body(Result.createSuccessResult(null)); + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java index 9e1f015..6162ce6 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java @@ -3,14 +3,18 @@ package com.justpickup.userservice.domain.user.dto; import lombok.Builder; import lombok.Getter; +import java.util.Date; + @Getter public class JwtTokenDto { private String accessToken; + private Date accessTokenExpiredDate; private String refreshToken; @Builder - public JwtTokenDto(String accessToken, String refreshToken) { + public JwtTokenDto(String accessToken, String refreshToken, Date accessTokenExpiredDate) { this.accessToken = accessToken; this.refreshToken = refreshToken; + this.accessTokenExpiredDate = accessTokenExpiredDate; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/global/exception/GlobalExceptionHandler.java b/user-service/src/main/java/com/justpickup/userservice/global/exception/GlobalExceptionHandler.java index 55d9c4a..e1e6473 100644 --- a/user-service/src/main/java/com/justpickup/userservice/global/exception/GlobalExceptionHandler.java +++ b/user-service/src/main/java/com/justpickup/userservice/global/exception/GlobalExceptionHandler.java @@ -36,7 +36,7 @@ public class GlobalExceptionHandler { // 쿠키 삭제 ResponseCookie responseCookie = cookieProvider.removeRefreshTokenCookie(); - return ResponseEntity.status(HttpStatus.FORBIDDEN) + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .header(HttpHeaders.SET_COOKIE, responseCookie.toString()) .body(e.getResult()); } diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java index 1b6ad13..464b9cd 100644 --- a/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java @@ -18,11 +18,12 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; -import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -70,6 +71,7 @@ public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFil String userId = user.getUsername(); String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles); + Date expiredTime = jwtTokenProvider.getExpiredTime(accessToken); String refreshToken = jwtTokenProvider.createJwtRefreshToken(); refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken)); @@ -83,17 +85,11 @@ public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFil response.addCookie(cookie); // body 설정 - Map tokens = Map.of( - "access_token", accessToken + Map tokens = Map.of( + "accessToken", accessToken, + "expiredTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredTime) ); new ObjectMapper().writeValue(response.getOutputStream(), Result.createSuccessResult(tokens)); } - - @Override - protected void unsuccessfulAuthentication - (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) - throws IOException, ServletException { - log.warn("로그인 실패!!"); - } } diff --git a/user-service/src/main/java/com/justpickup/userservice/global/utils/JwtTokenProvider.java b/user-service/src/main/java/com/justpickup/userservice/global/utils/JwtTokenProvider.java index 14c9d65..479174b 100644 --- a/user-service/src/main/java/com/justpickup/userservice/global/utils/JwtTokenProvider.java +++ b/user-service/src/main/java/com/justpickup/userservice/global/utils/JwtTokenProvider.java @@ -5,10 +5,6 @@ import io.jsonwebtoken.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import java.util.Date; @@ -57,8 +53,6 @@ public class JwtTokenProvider { .compact(); } - - public String getUserId(String token) { return getClaimsFromJwtToken(token).getSubject(); } @@ -75,6 +69,10 @@ public class JwtTokenProvider { return getClaimsFromJwtToken(token).get("value").toString(); } + public Date getExpiredTime(String token) { + return getClaimsFromJwtToken(token).getExpiration(); + } + public List getRoles(String token) { return (List) getClaimsFromJwtToken(token).get("roles"); } From d6c655c37f7059c755b771c644f6562f654a852f Mon Sep 17 00:00:00 2001 From: bum12ark Date: Tue, 1 Mar 2022 15:34:24 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat(owner-vue):=20url=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=20=EB=B0=8F=20interceptor=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - access token url 매핑 부분 변경 - interceptor router exception 에러 해결 --- owner-vue/src/api/auth.js | 2 +- owner-vue/src/api/user.js | 2 -- owner-vue/src/main.js | 37 +++++++++++++++++-------------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/owner-vue/src/api/auth.js b/owner-vue/src/api/auth.js index 32b2ef9..87c772b 100644 --- a/owner-vue/src/api/auth.js +++ b/owner-vue/src/api/auth.js @@ -21,6 +21,6 @@ export default { requestCheckAccessToken() { axios.defaults.headers.common['Authorization'] = "Bearer " + jwt.getToken(); - return axios.get("http://localhost:8001/user-service/auth/check/accessToken"); + return axios.get("http://localhost:8001/user-service/auth/check/access-token"); } } \ No newline at end of file diff --git a/owner-vue/src/api/user.js b/owner-vue/src/api/user.js index 9a694ee..622da40 100644 --- a/owner-vue/src/api/user.js +++ b/owner-vue/src/api/user.js @@ -23,8 +23,6 @@ export default { return true; } catch (err) { console.log("Error = ", err); - alert("로그인 실패!"); - return false; } diff --git a/owner-vue/src/main.js b/owner-vue/src/main.js index d630ed4..a459994 100644 --- a/owner-vue/src/main.js +++ b/owner-vue/src/main.js @@ -3,7 +3,7 @@ import App from './App.vue'; import vuetify from './plugins/vuetify'; import router from './router'; import axios from "axios"; -import auth from "./api/auth.js" +import auth from "./api/auth.js"; axios.defaults.withCredentials = true; @@ -20,26 +20,23 @@ axios.interceptors.response.use( return response; }, async (error) => { - try { - const originalRequest = error.config; - if (error.response.status === 401) { - // access token 만료 시 - if (error.response.data.code == "EXPIRED") { - const accessToken = await auth.requestReissue(); - - originalRequest.headers.Authorization = "Bearer " + accessToken; - return axios(originalRequest); - } - // 그외 에러일 시 - alert("로그인 정보가 일치하지 않습니다."); - await router.replace('/login'); - return; + 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 = "http://localhost:8080"; + alert("권한이 없습니다. 다시 로그인 해주세요"); } - } catch (error) { - alert("로그인 정보가 일치하지 않습니다."); - await router.replace('/login'); - return; - } + } + window.location.href = "http://localhost:8080"; + alert("권한이 없습니다. 다시 로그인해주세요."); + } return Promise.reject(error); } ); \ No newline at end of file From be175b1fe3d202a480c477253067c7041ef8eda4 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Tue, 1 Mar 2022 15:35:27 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor(owner-apigateway-service):=20jwt?= =?UTF-8?q?=20token=20validate=20=EB=A6=AC=ED=84=B4=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - boolean 에서 void로 변경 --- .../filter/AuthorizationHeaderFilter.java | 4 +--- .../security/JwtTokenProvider.java | 21 ++++--------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java index 406aeef..c723899 100644 --- a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java @@ -44,9 +44,7 @@ public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory) 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()); - throw e; - } 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; } } From 3ce0fd6f13eda481b7f118b0729002eef592457c Mon Sep 17 00:00:00 2001 From: bum12ark Date: Tue, 1 Mar 2022 15:52:54 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat(order-service):=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 주문 엔티티 생성 메소드 추가 - 서버 기동 시 테스트 데이터 추가 --- .../orderservice/OrderServiceApplication.java | 36 +++++++++++++++++++ .../domain/order/entity/Order.java | 35 ++++++++++++++++-- .../domain/orderItem/entity/OrderItem.java | 24 +++++++++++-- .../entity/OrderItemOption.java | 8 +++++ .../transaction/entity/Transaction.java | 9 +++++ 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/order-service/src/main/java/com/justpickup/orderservice/OrderServiceApplication.java b/order-service/src/main/java/com/justpickup/orderservice/OrderServiceApplication.java index 698d9fd..28ddf86 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/OrderServiceApplication.java +++ b/order-service/src/main/java/com/justpickup/orderservice/OrderServiceApplication.java @@ -1,9 +1,17 @@ package com.justpickup.orderservice; +import com.justpickup.orderservice.domain.order.entity.Order; +import com.justpickup.orderservice.domain.order.repository.OrderRepository; +import com.justpickup.orderservice.domain.orderItem.entity.OrderItem; +import com.justpickup.orderservice.domain.orderItemOption.entity.OrderItemOption; +import com.justpickup.orderservice.domain.transaction.entity.Transaction; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.Transactional; @SpringBootApplication @EnableEurekaClient @@ -14,4 +22,32 @@ public class OrderServiceApplication { SpringApplication.run(OrderServiceApplication.class, args); } + @Bean + @Transactional + CommandLineRunner run(OrderRepository orderRepository) { + return args -> { + Long userId = 1L; + Long userCouponId = null; + Long storeId = 1L; + Long orderPrice = 1000L; + + for (int i = 1; i <= 20; i++) { + Transaction transaction = Transaction.of(); + + Long itemId = 1L; + Long price = 100L; + Long count = 1L; + OrderItem orderItem = OrderItem.of(itemId + i, price * i, count + i, + OrderItemOption.of(), OrderItemOption.of()); + + OrderItem orderItem1 = OrderItem.of(itemId + i + 1, price * (i + 1), count + (i + 1), + OrderItemOption.of(), OrderItemOption.of()); + + Order order = Order.of(userId, userCouponId, storeId, orderPrice * i, transaction, orderItem, orderItem1); + + orderRepository.save(order); + } + }; + } + } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java index 7b2554e..f85b943 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java @@ -10,6 +10,7 @@ import org.hibernate.annotations.BatchSize; import javax.persistence.*; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Entity @@ -37,11 +38,39 @@ public class Order extends BaseEntity { @Enumerated(EnumType.STRING) private OrderStatus orderStatus; - @OneToOne(mappedBy = "order", fetch = FetchType.LAZY) + @OneToOne(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Transaction transaction; @BatchSize(size = 100) - @OneToMany(mappedBy = "order") - private List orderItems; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) + private List orderItems = new ArrayList<>(); + public static Order of(Long userId, Long userCouponId, Long storeId, Long orderPrice, + Transaction transaction, OrderItem... orderItems) { + Order order = new Order(); + order.userId = userId; + order.userCouponId = userCouponId; + order.storeId = storeId; + order.orderPrice = orderPrice; + + order.setTransaction(transaction); + for (OrderItem orderItem : orderItems) { + order.addOrderItem(orderItem); + } + + order.usedPoint = 0L; + order.orderStatus = OrderStatus.PLACED; + order.orderTime = LocalDateTime.now(); + return order; + } + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + transaction.setOrder(this); + } + + public void addOrderItem(OrderItem orderItem) { + this.orderItems.add(orderItem); + orderItem.setOrder(this); + } } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/orderItem/entity/OrderItem.java b/order-service/src/main/java/com/justpickup/orderservice/domain/orderItem/entity/OrderItem.java index 2b80748..b94e069 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/orderItem/entity/OrderItem.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/orderItem/entity/OrderItem.java @@ -9,6 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.*; +import java.util.ArrayList; import java.util.List; @Entity @@ -27,11 +28,30 @@ public class OrderItem extends BaseEntity { @JoinColumn(name = "order_id") private Order order; - @OneToMany(mappedBy = "orderItem") - private List orderItemOptions; + @OneToMany(mappedBy = "orderItem", cascade = CascadeType.ALL) + private List orderItemOptions = new ArrayList<>(); private Long price; private Long count; + public void setOrder(Order order) { + this.order = order; + } + + public void addOrderItemOption(OrderItemOption orderItemOption) { + this.orderItemOptions.add(orderItemOption); + orderItemOption.setOrderItem(this); + } + + public static OrderItem of(Long itemId, Long price, Long count, OrderItemOption... orderItemOptions) { + OrderItem orderItem = new OrderItem(); + orderItem.itemId = itemId; + orderItem.price = price; + orderItem.count = count; + for (OrderItemOption orderItemOption : orderItemOptions) { + orderItem.addOrderItemOption(orderItemOption); + } + return orderItem; + } } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/orderItemOption/entity/OrderItemOption.java b/order-service/src/main/java/com/justpickup/orderservice/domain/orderItemOption/entity/OrderItemOption.java index 28148bb..14ce23a 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/orderItemOption/entity/OrderItemOption.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/orderItemOption/entity/OrderItemOption.java @@ -24,4 +24,12 @@ public class OrderItemOption extends BaseEntity { private Long itemOptionId; + public void setOrderItem(OrderItem orderItem) { + this.orderItem = orderItem; + } + + public static OrderItemOption of() { + OrderItemOption orderItemOption = new OrderItemOption(); + return orderItemOption; + } } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/transaction/entity/Transaction.java b/order-service/src/main/java/com/justpickup/orderservice/domain/transaction/entity/Transaction.java index 3f230df..6be578c 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/transaction/entity/Transaction.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/transaction/entity/Transaction.java @@ -22,4 +22,13 @@ public class Transaction extends BaseEntity { @JoinColumn(name = "order_id") private Order order; + public void setOrder(Order order) { + this.order = order; + } + + public static Transaction of() { + Transaction transaction = new Transaction(); + return transaction; + } + }