Merge pull request #32 from Development-team-1/customer_login_page
feat(customer vue, user-service): customer vue OAuth 인증
This commit is contained in:
@@ -31,6 +31,8 @@ dependencies {
|
|||||||
// https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos
|
// 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 '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'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
package com.justpickup.customerapigatewayservice.filter;
|
package com.justpickup.customerapigatewayservice.filter;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.justpickup.customerapigatewayservice.security.JwtTokenProvider;
|
import com.justpickup.customerapigatewayservice.security.JwtTokenProvider;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
|
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
|
||||||
@@ -44,9 +51,7 @@ public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<Auth
|
|||||||
// JWT 토큰 판별
|
// JWT 토큰 판별
|
||||||
String token = authorizationHeader.replace("Bearer", "");
|
String token = authorizationHeader.replace("Bearer", "");
|
||||||
|
|
||||||
if (!jwtTokenProvider.validateJwtToken(token)) {
|
jwtTokenProvider.validateJwtToken(token);
|
||||||
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
String subject = jwtTokenProvider.getUserId(token);
|
String subject = jwtTokenProvider.getUserId(token);
|
||||||
if (false == jwtTokenProvider.getRoles(token).contains("Customer")) {
|
if (false == jwtTokenProvider.getRoles(token).contains("Customer")) {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.justpickup.customerapigatewayservice.filter;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
|
||||||
|
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||||
|
List<Class<? extends RuntimeException>> jwtExceptions =
|
||||||
|
List.of(SignatureException.class,
|
||||||
|
MalformedJwtException.class,
|
||||||
|
UnsupportedJwtException.class,
|
||||||
|
IllegalArgumentException.class);
|
||||||
|
Class<? extends Throwable> exceptionClass = ex.getClass();
|
||||||
|
|
||||||
|
Map<String, Object> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -73,25 +73,12 @@ public class JwtTokenProvider {
|
|||||||
return (List<String>) getClaimsFromJwtToken(token).get("roles");
|
return (List<String>) getClaimsFromJwtToken(token).get("roles");
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateJwtToken(String token) {
|
public void validateJwtToken(String token) {
|
||||||
try {
|
try {
|
||||||
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
|
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
|
||||||
return true;
|
} catch (SignatureException | MalformedJwtException |
|
||||||
} catch (SignatureException e) {
|
UnsupportedJwtException | IllegalArgumentException | ExpiredJwtException jwtException) {
|
||||||
log.error("Invalid JWT signature: {}", e.getMessage());
|
throw jwtException;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,25 @@ spring:
|
|||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
gateway:
|
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:
|
routes:
|
||||||
- id: owner-frontend-service
|
- id: owner-frontend-service
|
||||||
uri: lb://CUSTOMER-FRONTEND-SERVICE
|
uri: lb://CUSTOMER-FRONTEND-SERVICE
|
||||||
@@ -33,6 +52,7 @@ spring:
|
|||||||
predicates:
|
predicates:
|
||||||
- Path=/store-service/**
|
- Path=/store-service/**
|
||||||
filters:
|
filters:
|
||||||
|
- AuthorizationHeaderFilter
|
||||||
- RewritePath=/store-service/(?<segment>.*),/$\{segment}
|
- RewritePath=/store-service/(?<segment>.*),/$\{segment}
|
||||||
- id: user-service
|
- id: user-service
|
||||||
uri: lb://USER-SERVICE
|
uri: lb://USER-SERVICE
|
||||||
@@ -55,6 +75,13 @@ spring:
|
|||||||
- Method=POST
|
- Method=POST
|
||||||
filters:
|
filters:
|
||||||
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
|
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
|
||||||
|
- id: user-service
|
||||||
|
uri: lb://USER-SERVICE
|
||||||
|
predicates:
|
||||||
|
- Path=/user-service/auth/reissue
|
||||||
|
- Method=GET
|
||||||
|
filters:
|
||||||
|
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
|
||||||
- id: user-service
|
- id: user-service
|
||||||
uri: lb://USER-SERVICE
|
uri: lb://USER-SERVICE
|
||||||
predicates:
|
predicates:
|
||||||
|
|||||||
3
customer-vue/.env
Normal file
3
customer-vue/.env
Normal file
@@ -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
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
"@vue/cli-plugin-babel": "^5.0.0",
|
"@vue/cli-plugin-babel": "^5.0.0",
|
||||||
"@vue/cli-plugin-eslint": "^5.0.0",
|
"@vue/cli-plugin-eslint": "^5.0.0",
|
||||||
"@vue/cli-service": "^5.0.0",
|
"@vue/cli-service": "^5.0.0",
|
||||||
|
"axios": "^0.26.0",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
"vue-template-compiler": "^2.6.14"
|
"vue-template-compiler": "^2.6.14"
|
||||||
|
|||||||
26
customer-vue/src/api/auth.js
Normal file
26
customer-vue/src/api/auth.js
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
34
customer-vue/src/api/store.js
Normal file
34
customer-vue/src/api/store.js
Normal file
@@ -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'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
32
customer-vue/src/api/user.js
Normal file
32
customer-vue/src/api/user.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BIN
customer-vue/src/assets/justLogo.png
Normal file
BIN
customer-vue/src/assets/justLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
customer-vue/src/assets/logo_google.png
Normal file
BIN
customer-vue/src/assets/logo_google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
19
customer-vue/src/assets/logo_naver.svg
Normal file
19
customer-vue/src/assets/logo_naver.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="53" height="52" viewBox="0 0 53 52">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g transform="translate(-794.000000, -2855.000000) translate(166.000000, 2282.000000) translate(0.500000, 381.500000) translate(486.500000, 191.500000) translate(73.500000, 0.000000) translate(68.000000, 0.000000)">
|
||||||
|
<circle cx="26" cy="26" r="26" fill="#1EC800"/>
|
||||||
|
<path fill="#FFF" fill-opacity="0" d="M12.9 13.265H38.9V39.265H12.9z"/>
|
||||||
|
<path fill="#FFF" d="M28.997 17.524L28.997 26.343 22.823 17.524 16.15 17.524 16.15 35.006 22.801 35.006 22.801 26.186 28.976 35.006 35.65 35.006 35.65 17.524z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1000 B |
38
customer-vue/src/common/jwt.js
Normal file
38
customer-vue/src/common/jwt.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ import Vue from 'vue';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import vuetify from "@/plugins/vuetify";
|
import vuetify from "@/plugins/vuetify";
|
||||||
import router from "./router/router.js";
|
import router from "./router/router.js";
|
||||||
|
import axios from "axios";
|
||||||
|
import auth from "@/api/auth";
|
||||||
|
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
@@ -10,3 +13,29 @@ new Vue({
|
|||||||
router,
|
router,
|
||||||
render: h => h(App),
|
render: h => h(App),
|
||||||
}).$mount('#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);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -2,9 +2,17 @@ import Vue from 'vue';
|
|||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
|
|
||||||
import HomeLayout from '../views/Layout/HomeLayout.vue';
|
import HomeLayout from '../views/Layout/HomeLayout.vue';
|
||||||
|
const ACCESS_TOKEN_NAME = "accessToken";
|
||||||
|
const EXPIRED_TIME_NAME = "expiredTime";
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -17,7 +25,26 @@ const routes = [
|
|||||||
component: () => import('../views/HomeView')
|
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({
|
const router = new VueRouter({
|
||||||
|
|||||||
20
customer-vue/src/views/Layout/AuthSuccess.vue
Normal file
20
customer-vue/src/views/Layout/AuthSuccess.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<div>인증에 성공하였습니다.</div>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AuthSuccess",
|
||||||
|
components: {
|
||||||
|
},mounted() {
|
||||||
|
opener.document.location.href=process.env.VUE_APP_BASEURL;
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
120
customer-vue/src/views/LoginPage.vue
Normal file
120
customer-vue/src/views/LoginPage.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
fill-height
|
||||||
|
>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div align="center" ><v-img
|
||||||
|
max-height="150"
|
||||||
|
max-width="250"
|
||||||
|
:src="logo"></v-img></div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<v-col class="align-content-center">
|
||||||
|
|
||||||
|
|
||||||
|
<v-form ref="form" lazy-validation>
|
||||||
|
<v-text-field
|
||||||
|
:rules="[v => /.+@.+\..+/.test(v) || 'E-mail must be valid', v => !!v || '이메일은 필수 값입니다']"
|
||||||
|
label="이메일"
|
||||||
|
prepend-icon="mdi-account-circle"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
:rules="[v => !!v || '비밀번호는 필수 값입니다']"
|
||||||
|
label="비밀번호"
|
||||||
|
type="Password"
|
||||||
|
prepend-icon="mdi-lock"
|
||||||
|
append-icon="mdi-eye-off"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</v-btn>
|
||||||
|
<div class="d-block my-7" align="center">
|
||||||
|
<v-subheader class="d-inline" >소셜 아이디로 로그인해보세요!</v-subheader>
|
||||||
|
</div>
|
||||||
|
<div class="d-block " align="center">
|
||||||
|
<v-btn
|
||||||
|
class="mx-2"
|
||||||
|
fab
|
||||||
|
small
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
class="d-inline-block align-lg-center mx-5"
|
||||||
|
style=""
|
||||||
|
max-width="38"
|
||||||
|
max-height="38"
|
||||||
|
:src="logo_naver"
|
||||||
|
@click="login_auth('naver')"
|
||||||
|
/>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="mx-2"
|
||||||
|
fab
|
||||||
|
small
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
class="d-inline-block mx-5"
|
||||||
|
max-width="38"
|
||||||
|
max-height="38"
|
||||||
|
min-width="38"
|
||||||
|
min-height="38"
|
||||||
|
:src="logo_google"
|
||||||
|
@click="login_auth('google')"
|
||||||
|
/>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-form>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import logo from '@/assets/justLogo.png'
|
||||||
|
import logo_naver from '@/assets/logo_naver.svg'
|
||||||
|
import logo_google from '@/assets/logo_google.png'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "LoginPage",
|
||||||
|
data (){
|
||||||
|
return {
|
||||||
|
logo : logo,
|
||||||
|
logo_naver: logo_naver,
|
||||||
|
logo_google : logo_google,
|
||||||
|
auth_popup: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
auth_popup : function () {
|
||||||
|
this.auth_popup.addEventListener('beforeunload', function() {
|
||||||
|
window.location.href=process.env.VUE_APP_BASEURL;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
login_auth: async function(target) {
|
||||||
|
const _url = process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL+'/user-service/oauth2/authorization/'+target
|
||||||
|
this.auth_popup = window.open(
|
||||||
|
_url,
|
||||||
|
"",
|
||||||
|
"width=600,height=400,left=200,top=200"
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
transpileDependencies: true
|
transpileDependencies: true,
|
||||||
|
devServer:{
|
||||||
|
allowedHosts: 'all',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import reactor.core.publisher.Mono;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
||||||
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
|
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
|
||||||
private static final String TEST_CIRCUIT_BREAKER = "testCircuitBreaker";
|
|
||||||
|
|
||||||
public GlobalFilter(){
|
public GlobalFilter(){
|
||||||
super(Config.class);
|
super(Config.class);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -75,10 +76,20 @@ public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2
|
|||||||
.map(GrantedAuthority::getAuthority)
|
.map(GrantedAuthority::getAuthority)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Long customerId = customer.getId();
|
return new DefaultOAuth2User(
|
||||||
|
authorities
|
||||||
|
, attributeDto.getAttributes()
|
||||||
|
, attributeDto.getNameAttributeKey());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Authentication authentication) throws IOException, ServletException {
|
||||||
|
String userEmail = String.valueOf(((DefaultOAuth2User) authentication.getPrincipal()).getAttributes().get("email"));
|
||||||
|
|
||||||
|
|
||||||
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
|
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
|
||||||
|
Long customerId = customerRepository.findByEmail(userEmail).get().getId();
|
||||||
refreshTokenService.updateRefreshToken(customerId, jwtTokenProvider.getRefreshTokenId(refreshToken));
|
refreshTokenService.updateRefreshToken(customerId, jwtTokenProvider.getRefreshTokenId(refreshToken));
|
||||||
|
|
||||||
// 쿠키 설정
|
// 쿠키 설정
|
||||||
@@ -89,13 +100,19 @@ public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2
|
|||||||
response.setContentType(APPLICATION_JSON_VALUE);
|
response.setContentType(APPLICATION_JSON_VALUE);
|
||||||
response.addCookie(cookie);
|
response.addCookie(cookie);
|
||||||
|
|
||||||
return new DefaultOAuth2User(
|
// body 설정
|
||||||
authorities
|
String accessToken = jwtTokenProvider.createJwtAccessToken(String.valueOf(customerId), request.getRequestURI(), authentication.getAuthorities().stream()
|
||||||
, attributeDto.getAttributes()
|
.map(GrantedAuthority::getAuthority)
|
||||||
, attributeDto.getNameAttributeKey());
|
.collect(Collectors.toList()));
|
||||||
|
Date expiredTime = jwtTokenProvider.getExpiredTime(accessToken);
|
||||||
|
|
||||||
|
response.sendRedirect("http://just-pickup.com:8080/auth?" +
|
||||||
|
"accessToken="+accessToken+
|
||||||
|
"&expiredTime="+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredTime));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Customer saveCustomer(OAuthAttributeDto attributeDto){
|
public Customer saveCustomer(OAuthAttributeDto attributeDto){
|
||||||
return customerRepository.save(
|
return customerRepository.save(
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
.deleteCookies("refresh-token");
|
.deleteCookies("refresh-token");
|
||||||
|
|
||||||
http.oauth2Login()
|
http.oauth2Login()
|
||||||
.defaultSuccessUrl("http://just-pickup.com:8080/")
|
|
||||||
.userInfoEndpoint()
|
.userInfoEndpoint()
|
||||||
.userService(oAuthService)
|
.userService(oAuthService)
|
||||||
.and()
|
.and()
|
||||||
.failureUrl("http://just-pickup.com:8080/login");
|
.failureUrl("http://just-pickup.com:8080/login")
|
||||||
|
.successHandler(oAuthService::onAuthenticationSuccess);
|
||||||
|
|
||||||
http.addFilter(loginAuthenticationFilter);
|
http.addFilter(loginAuthenticationFilter);
|
||||||
// http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
|
// http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user