From 635db824fd4cb5ef3b812e23baf8ff82f339e457 Mon Sep 17 00:00:00 2001 From: bw1611 <97327583+leebuwon@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:44:51 +0900 Subject: [PATCH] Feat/issue-54 (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: API 라우팅을 위한 yml 설정, cors 설정 * Feat: chatting-service yml 추가, authFilter 추가 * Feat: AuthFilter 동작 * Refactoring: else-if 제거 * Feat: jwt token secret key를 secret yml로 설정 --- backend/apigateway-service/.gitignore | 2 + backend/apigateway-service/build.gradle | 6 + .../apigatewayservice/config/CORSConfig.java | 20 ++++ .../apigatewayservice/filter/AuthFilter.java | 105 ++++++++++++++++++ .../src/main/resources/application.yml | 38 ++++++- 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/config/CORSConfig.java create mode 100644 backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/filter/AuthFilter.java diff --git a/backend/apigateway-service/.gitignore b/backend/apigateway-service/.gitignore index c2065bc..8506e5b 100644 --- a/backend/apigateway-service/.gitignore +++ b/backend/apigateway-service/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +application-secret.yml \ No newline at end of file diff --git a/backend/apigateway-service/build.gradle b/backend/apigateway-service/build.gradle index 8cb89a8..81a05f2 100644 --- a/backend/apigateway-service/build.gradle +++ b/backend/apigateway-service/build.gradle @@ -28,6 +28,12 @@ ext { dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/config/CORSConfig.java b/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/config/CORSConfig.java new file mode 100644 index 0000000..f6eff94 --- /dev/null +++ b/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/config/CORSConfig.java @@ -0,0 +1,20 @@ +package com.tadak.apigatewayservice.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +public class CORSConfig implements WebFluxConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry){ + registry.addMapping("/**") + .allowCredentials(true) + .allowedOrigins("http://127.0.0.1:5173") + .allowedHeaders("*") + .allowedMethods("*") + .allowedOriginPatterns("*") + .exposedHeaders("Accesstoken, Refreshtoken"); + } +} diff --git a/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/filter/AuthFilter.java b/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/filter/AuthFilter.java new file mode 100644 index 0000000..3cdc64e --- /dev/null +++ b/backend/apigateway-service/src/main/java/com/tadak/apigatewayservice/filter/AuthFilter.java @@ -0,0 +1,105 @@ +package com.tadak.apigatewayservice.filter; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +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.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.security.Key; + +@Slf4j +@Component +public class AuthFilter extends AbstractGatewayFilterFactory { + + private final String secret; + public static final String ACCESS_AUTHORIZATION_HEADER = "Accesstoken"; + public static final String REFRESH_AUTHORIZATION_HEADER = "Refreshtoken"; + private Key key; + + + public AuthFilter(@Value("${jwt.secret}") String secret){ + super(Config.class); + this.secret = secret; + } + + @PostConstruct + public void afterPropertiesSet() { + byte[] keyBytes = Decoders.BASE64.decode(secret); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + + if (request.getHeaders().containsKey(ACCESS_AUTHORIZATION_HEADER)) { + String accessToken = request.getHeaders().get(ACCESS_AUTHORIZATION_HEADER).get(0); + + if (isJwtValid(accessToken)) { + log.info("AccessToken 인가받은 사용자입니다."); + return chain.filter(exchange); + } + } + + String refreshToken = request.getHeaders().get(REFRESH_AUTHORIZATION_HEADER).get(0); + + if (refreshToken != null && isJwtValid(refreshToken)) { + return chain.filter(exchange); + } + + return onError(exchange, "토큰이 유효하지 않습니다.", HttpStatus.UNAUTHORIZED); + }; + } + + private Mono onError(ServerWebExchange exchange, String error, HttpStatus httpStatus) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(httpStatus); + + log.error(error); + return response.setComplete(); + } + + private boolean isJwtValid(String token) { + boolean returnValue = true; + + if (StringUtils.hasText(token) && token.startsWith("Bearer ")){ + token = token.substring(7); + } + + log.info("token = {}", token); + + String subject = null; + + try{ + subject = Jwts.parserBuilder() + .setSigningKey(key) + .build().parseClaimsJws(token) + .getBody() + .getSubject(); + }catch (Exception e){ + returnValue = false; + } + + if (subject == null || subject.isEmpty()){ + returnValue = false; + } + + return returnValue; + } + + public static class Config { + + } +} diff --git a/backend/apigateway-service/src/main/resources/application.yml b/backend/apigateway-service/src/main/resources/application.yml index efebd35..f705d8e 100644 --- a/backend/apigateway-service/src/main/resources/application.yml +++ b/backend/apigateway-service/src/main/resources/application.yml @@ -9,5 +9,41 @@ eureka: defaultZone: http://localhost:8761/eureka spring: + profiles: + include: secret application: - name: apigateway-service \ No newline at end of file + name: apigateway-service + cloud: + gateway: + default-filters: + - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials + routes: + - id: user-service # 회원가입 + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/signup/** + - Method=POST, GET + - id: user-service # 로그인 + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/login + - Method=POST + - id: user-service # 이메일 인증 + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/authcode/** + - Method=POST, GET + - id: user-service # 권한 인증 테스트 + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/hello + - Method=GET + filters: + - AuthFilter + - id: chatting-service # chatting-service + uri: lb://CHATTING-SERVICE + predicates: + - Path=/chatting-service/** + - Method=POST, GET + filters: + - AuthFilter \ No newline at end of file