Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ out/
/.nb-gradle/

### VS Code ###
.vscode/
.vscode/

.env
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-security'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.michelet.gateway.infrastructure.config;

import java.util.List;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
@EnableConfigurationProperties(CorsProperties.class)
public class CorsConfig {
private final CorsProperties corsProperties;

public CorsConfig(CorsProperties corsProperties) {
this.corsProperties = corsProperties;
}

@Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(corsProperties.allowedOrigins());
config.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.DELETE.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.PATCH.name(),
HttpMethod.OPTIONS.name()
));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true); //์ฟ ํ‚ค์— refreshToken

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.michelet.gateway.infrastructure.config;

import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "cors")
public record CorsProperties(
List<String> allowedOrigins
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.michelet.gateway.infrastructure.config;

import com.michelet.gateway.infrastructure.security.GatewayRoleConverter;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, GatewayRoleConverter roleConverter){
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(roleConverter);
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(
exchanges-> exchanges.pathMatchers(
"/api/*/auth/login",
"/api/*/auth/reissue",
"/api/*/users/signup"
).permitAll()
.pathMatchers("/api/*/admin/**").hasRole("MASTER")
.anyExchange().authenticated()

)
.oauth2ResourceServer(oauth2-> oauth2.jwt(
jwtSpec -> jwtSpec.jwtAuthenticationConverter(jwtAuthenticationConverter)
)).build();
}

@Bean
public ReactiveJwtDecoder reactiveJwtDecoder(@Value("${jwt.secret}") String secret){
SecretKey secretKey = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
return NimbusReactiveJwtDecoder.withSecretKey(secretKey).build();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.michelet.gateway.infrastructure.security;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;

@Component
public class GatewayRoleConverter implements Converter<Jwt, Flux<GrantedAuthority>> {

@Override
public Flux<GrantedAuthority> convert(Jwt jwt) {
String role = jwt.getClaimAsString("role");

if(role == null || role.isBlank())
return Flux.empty();

return Flux.just(new SimpleGrantedAuthority("ROLE_" +role));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.michelet.gateway.infrastructure.security;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_ROLE_HEADER = "X-User-Role";


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Authentication authentication = securityContext.getAuthentication();
if(authentication == null){
return chain.filter(exchange);
}
return addHeaders(authentication,exchange,chain);
})
.switchIfEmpty(chain.filter(exchange));
}

public Mono<Void> addHeaders(
Authentication authentication,
ServerWebExchange exchange,
GatewayFilterChain chain
){
if(!(authentication instanceof JwtAuthenticationToken))
return chain.filter(exchange);
Jwt jwt = ((JwtAuthenticationToken) authentication).getToken();

ServerHttpRequest mutatedRequest = exchange.getRequest()
.mutate()
.headers(
httpHeaders -> {
httpHeaders.remove(USER_ID_HEADER);
httpHeaders.remove(USER_ROLE_HEADER);

String userId = jwt.getSubject();
if (userId != null && !userId.isBlank()) {
httpHeaders.set(USER_ID_HEADER, userId);
}

String role = jwt.getClaimAsString("role");
if (role != null && !role.isBlank()) {
httpHeaders.set(USER_ROLE_HEADER, role);
}
}
)
.build();
Comment thread
Sehi55 marked this conversation as resolved.

return chain.filter(exchange.mutate().request(mutatedRequest).build());
}

@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 5;
}

}
2 changes: 1 addition & 1 deletion src/main/resources/application-docker.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
defaultZone: http://${EUREKA_CONTAINER_NAME}:${EUREKA_PORT}/eureka/
Comment thread
Sehi55 marked this conversation as resolved.
6 changes: 5 additions & 1 deletion src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
defaultZone: http://localhost:${EUREKA_PORT:8761}/eureka/

cors:
allowed-origins:
- http://localhost:3000
9 changes: 8 additions & 1 deletion src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ eureka:
spring:
cloud:
discovery:
enabled: false
enabled: false

jwt:
secret: test-secret-key-test-secret-key-test-secret-key

cors:
allowed-origins:
- http://localhost:3000
8 changes: 7 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ spring:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/v1/users/**,/api/v1/admin/users/**
- Path=/api/v1/users/**,/api/v1/admin/users/**,/api/v1/auth/**,/api/v1/admin/auth/**

- id: timeslot-service
uri: lb://TIMESLOT-SERVICE
Expand Down Expand Up @@ -61,3 +61,9 @@ spring:

server:
port: 19000

jwt:
secret: ${JWT_SECRET}

cors:
allowed-origins: []
Comment thread
Sehi55 marked this conversation as resolved.
Loading