auth gateway

This commit is contained in:
2026-03-08 01:00:58 +01:00
parent 507f92d983
commit 1c1965a082
17 changed files with 718 additions and 78 deletions

View File

@@ -43,6 +43,50 @@
<artifactId>spring-cloud-starter-gateway</artifactId> <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency> </dependency>
<!-- Reactive Security (WebFlux auto-detected) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- R2DBC — reactive DB access -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- R2DBC PostgreSQL driver (version managed by Spring Boot BOM) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- Validation (@Valid, @NotBlank, @Email) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Consul service discovery --> <!-- Consul service discovery -->
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>

View File

@@ -0,0 +1,19 @@
package com.posthub.gateway.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "auth")
@Getter
@Setter
public class AuthProperties {
private List<String> publicPaths = new ArrayList<>();
private List<String> adminPaths = new ArrayList<>();
}

View File

@@ -0,0 +1,63 @@
package com.posthub.gateway.config;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.lang.NonNull;
import java.util.List;
@Configuration
public class R2dbcConfig {
@Bean
public R2dbcCustomConversions r2dbcCustomConversions() {
return R2dbcCustomConversions.of(
PostgresDialect.INSTANCE.getStoreConversions(),
List.of(
new RegistrationStatusReadConverter(),
new RegistrationStatusWriteConverter(),
new UserRoleReadConverter(),
new UserRoleWriteConverter()
)
);
}
@ReadingConverter
static class RegistrationStatusReadConverter implements Converter<String, RegistrationStatus> {
@Override
public RegistrationStatus convert(@NonNull String source) {
return RegistrationStatus.valueOf(source);
}
}
@WritingConverter
static class RegistrationStatusWriteConverter implements Converter<RegistrationStatus, String> {
@Override
public String convert(@NonNull RegistrationStatus source) {
return source.name();
}
}
@ReadingConverter
static class UserRoleReadConverter implements Converter<String, UserRole> {
@Override
public UserRole convert(@NonNull String source) {
return UserRole.valueOf(source);
}
}
@WritingConverter
static class UserRoleWriteConverter implements Converter<UserRole, String> {
@Override
public String convert(@NonNull UserRole source) {
return source.name();
}
}
}

View File

@@ -0,0 +1,56 @@
package com.posthub.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.logout(ServerHttpSecurity.LogoutSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers(
"/api/auth/login",
"/api/auth/register",
"/api/auth/refresh/token"
).permitAll()
.pathMatchers(
"/actuator/**",
"/v3/api-docs/**",
"/swagger-ui/**"
).permitAll()
.anyExchange().permitAll()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((exchange, ex) -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return Mono.empty();
})
.accessDeniedHandler((exchange, denied) -> {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return Mono.empty();
})
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,40 @@
package com.posthub.gateway.controller;
import com.posthub.gateway.model.request.LoginUserRequest;
import com.posthub.gateway.model.request.RegistrationUserRequest;
import com.posthub.gateway.model.response.JwtResponse;
import com.posthub.gateway.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
public Mono<ResponseEntity<JwtResponse>> register(@Valid @RequestBody RegistrationUserRequest request) {
return authService.register(request)
.map(response -> ResponseEntity.status(HttpStatus.CREATED).body(response));
}
@PostMapping("/login")
public Mono<ResponseEntity<JwtResponse>> login(@Valid @RequestBody LoginUserRequest request) {
return authService.login(request)
.map(ResponseEntity::ok);
}
@GetMapping("/refresh/token")
public Mono<ResponseEntity<JwtResponse>> refreshToken(
@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) {
return authService.refreshToken(authHeader)
.map(ResponseEntity::ok);
}
}

View File

@@ -1,63 +0,0 @@
package com.posthub.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
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 reactor.core.publisher.Mono;
import java.util.List;
/**
* Per-route filter: validates that Authorization header contains a Bearer token.
* Does NOT verify JWT signature — downstream services handle that.
*
* Usage in application.yml:
* filters:
* - AuthFilter
*/
@Slf4j
@Component
public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> {
public AuthFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
List<String> authHeaders = request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION);
if (authHeaders.isEmpty()) {
log.warn("Missing Authorization header for {}", path);
return unauthorized(exchange);
}
String token = authHeaders.getFirst();
if (token == null || !token.startsWith("Bearer ") || !token.contains(".")) {
log.warn("Invalid Authorization header format for {}", path);
return unauthorized(exchange);
}
log.debug("Token present for {}", path);
return chain.filter(exchange);
};
}
private Mono<Void> unauthorized(org.springframework.web.server.ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
public static class Config {
}
}

View File

@@ -0,0 +1,116 @@
package com.posthub.gateway.filter;
import com.posthub.gateway.config.AuthProperties;
import com.posthub.gateway.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTokenProvider jwtTokenProvider;
private final AuthProperties authProperties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 1. Public paths — пропустить без JWT
if (isPublicPath(path)) {
return chain.filter(exchange);
}
// 2. Извлечь токен
String token = extractToken(request);
if (token == null) {
log.warn("Missing JWT for path: {}", path);
return unauthorized(exchange);
}
// 3. Валидация JWT
if (!jwtTokenProvider.validateToken(token)) {
log.warn("Invalid JWT for path: {}", path);
return unauthorized(exchange);
}
// 4. Проверка роли для admin-paths
String role = jwtTokenProvider.getUserRole(token);
if (isAdminPath(path) && !"ADMIN".equals(role)) {
log.warn("Access denied for role {} to admin path: {}", role, path);
return forbidden(exchange);
}
// 5. Добавить headers в downstream-запрос
String userId = jwtTokenProvider.getUserId(token);
String email = jwtTokenProvider.getUserEmail(token);
String username = jwtTokenProvider.getUsername(token);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-User-Email", email)
.header("X-User-Name", username)
.header("X-User-Role", role)
.build();
log.debug("JWT validated for user {} (role={}) -> {}", email, role, path);
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
private String extractToken(ServerHttpRequest request) {
List<String> authHeaders = request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION);
if (authHeaders.isEmpty()) {
return null;
}
String header = authHeaders.getFirst();
if (header != null && header.startsWith("Bearer ") && header.contains(".")) {
return header.substring(7);
}
return null;
}
private boolean isPublicPath(String path) {
return authProperties.getPublicPaths().stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
private boolean isAdminPath(String path) {
return authProperties.getAdminPaths().stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
private Mono<Void> forbidden(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
@Override
public int getOrder() {
// После RequestLoggingFilter (HIGHEST_PRECEDENCE), но до route filters
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}

View File

@@ -0,0 +1,43 @@
package com.posthub.gateway.model.entity;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Table("users")
@Getter
@Setter
@NoArgsConstructor
public class User {
@Id
private Integer id;
private String username;
private String password;
private String email;
private LocalDateTime created;
private LocalDateTime updated;
@Column("last_login")
private LocalDateTime lastLogin;
private Boolean deleted;
@Column("registration_status")
private RegistrationStatus registrationStatus;
@Column("role")
private UserRole role;
}

View File

@@ -0,0 +1,7 @@
package com.posthub.gateway.model.enums;
public enum RegistrationStatus {
ACTIVE,
BLOCKED,
DELETED
}

View File

@@ -0,0 +1,6 @@
package com.posthub.gateway.model.enums;
public enum UserRole {
USER,
ADMIN
}

View File

@@ -0,0 +1,18 @@
package com.posthub.gateway.model.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginUserRequest {
@NotBlank
@Email
private String email;
@NotBlank
private String password;
}

View File

@@ -0,0 +1,28 @@
package com.posthub.gateway.model.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RegistrationUserRequest {
@NotBlank
@Size(min = 2, max = 30)
private String username;
@NotBlank
@Email
@Size(max = 50)
private String email;
@NotBlank
@Size(min = 6, max = 80)
private String password;
@NotBlank
private String confirmPassword;
}

View File

@@ -0,0 +1,14 @@
package com.posthub.gateway.model.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class JwtResponse {
private String token;
private String refreshToken;
private String email;
private String username;
}

View File

@@ -0,0 +1,14 @@
package com.posthub.gateway.repository;
import com.posthub.gateway.model.entity.User;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
public interface UserRepository extends ReactiveCrudRepository<User, Integer> {
Mono<User> findByEmail(String email);
Mono<User> findByUsername(String username);
Mono<Boolean> existsByEmail(String email);
}

View File

@@ -0,0 +1,111 @@
package com.posthub.gateway.security;
import com.posthub.gateway.model.entity.User;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtTokenProvider {
private static final String USER_ID = "userId";
private static final String USERNAME = "username";
private static final String USER_EMAIL = "email";
private static final String USER_ROLE = "role";
private static final String USER_REGISTRATION_STATUS = "registrationStatus";
private static final String SESSION_ID = "sessionId";
private static final String LAST_UPDATE = "lastUpdate";
private final SecretKey secretKey;
private final long jwtValidityInMilliseconds;
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration:103600000}") long jwtValidityInMilliseconds) {
byte[] decoded = Decoders.BASE64.decode(secret);
this.secretKey = Keys.hmacShaKeyFor(decoded);
this.jwtValidityInMilliseconds = jwtValidityInMilliseconds;
}
public String generateToken(@NonNull User user, String sessionId) {
Map<String, Object> claims = new HashMap<>();
claims.put(USER_ID, user.getId());
claims.put(USERNAME, user.getUsername());
claims.put(USER_EMAIL, user.getEmail());
claims.put(USER_ROLE, user.getRole().name());
claims.put(USER_REGISTRATION_STATUS, user.getRegistrationStatus().name());
claims.put(SESSION_ID, sessionId);
claims.put(LAST_UPDATE, LocalDateTime.now().toString());
return createToken(claims, user.getEmail());
}
public String refreshToken(String token) {
Claims claims = getAllClaimsFromToken(token);
return createToken(claims, claims.getSubject());
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return !claims.getPayload().getExpiration().before(new Date());
} catch (JwtException | IllegalArgumentException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
return false;
}
}
public String getUsername(String token) {
return getAllClaimsFromToken(token).get(USERNAME, String.class);
}
public String getUserId(String token) {
return String.valueOf(getAllClaimsFromToken(token).get(USER_ID));
}
public String getUserEmail(String token) {
return getAllClaimsFromToken(token).get(USER_EMAIL, String.class);
}
public String getUserRole(String token) {
return getAllClaimsFromToken(token).get(USER_ROLE, String.class);
}
public String getSessionId(String token) {
return getAllClaimsFromToken(token).get(SESSION_ID, String.class);
}
private Claims getAllClaimsFromToken(String token) {
try {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds))
.signWith(secretKey)
.compact();
}
}

View File

@@ -0,0 +1,108 @@
package com.posthub.gateway.service;
import com.posthub.gateway.model.entity.User;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import com.posthub.gateway.model.request.LoginUserRequest;
import com.posthub.gateway.model.request.RegistrationUserRequest;
import com.posthub.gateway.model.response.JwtResponse;
import com.posthub.gateway.repository.UserRepository;
import com.posthub.gateway.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
public Mono<JwtResponse> register(RegistrationUserRequest request) {
if (!request.getPassword().equals(request.getConfirmPassword())) {
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, "Passwords do not match"));
}
return userRepository.existsByEmail(request.getEmail())
.flatMap(exists -> {
if (exists) {
return Mono.error(new ResponseStatusException(HttpStatus.CONFLICT, "Email already registered"));
}
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRegistrationStatus(RegistrationStatus.ACTIVE);
user.setRole(UserRole.USER);
user.setCreated(LocalDateTime.now());
user.setUpdated(LocalDateTime.now());
user.setDeleted(false);
return userRepository.save(user);
})
.map(savedUser -> {
String sessionId = UUID.randomUUID().toString();
String token = jwtTokenProvider.generateToken(savedUser, sessionId);
String refreshToken = jwtTokenProvider.refreshToken(token);
log.info("User registered: {}", savedUser.getEmail());
return new JwtResponse(token, refreshToken, savedUser.getEmail(), savedUser.getUsername());
});
}
public Mono<JwtResponse> login(LoginUserRequest request) {
return userRepository.findByEmail(request.getEmail())
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials")))
.flatMap(user -> {
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials"));
}
if (user.getRegistrationStatus() != RegistrationStatus.ACTIVE) {
return Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN, "Account is not active"));
}
user.setLastLogin(LocalDateTime.now());
user.setUpdated(LocalDateTime.now());
return userRepository.save(user);
})
.map(user -> {
String sessionId = UUID.randomUUID().toString();
String token = jwtTokenProvider.generateToken(user, sessionId);
String refreshToken = jwtTokenProvider.refreshToken(token);
log.info("User logged in: {}", user.getEmail());
return new JwtResponse(token, refreshToken, user.getEmail(), user.getUsername());
});
}
public Mono<JwtResponse> refreshToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing or invalid Authorization header"));
}
String token = authHeader.substring(7);
if (!jwtTokenProvider.validateToken(token)) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or expired token"));
}
String email = jwtTokenProvider.getUserEmail(token);
return userRepository.findByEmail(email)
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")))
.map(user -> {
String sessionId = jwtTokenProvider.getSessionId(token);
String newToken = jwtTokenProvider.generateToken(user, sessionId);
String newRefreshToken = jwtTokenProvider.refreshToken(newToken);
return new JwtResponse(newToken, newRefreshToken, user.getEmail(), user.getUsername());
});
}
}

View File

@@ -6,6 +6,16 @@ spring:
application: application:
name: gateway-service name: gateway-service
# ---- R2DBC (reactive DB) ----
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
username: ${DB_USERNAME:app}
password: ${DB_PASSWORD:}
pool:
initial-size: 2
max-size: 10
max-idle-time: 30m
cloud: cloud:
consul: consul:
host: ${CONSUL_HOST:localhost} host: ${CONSUL_HOST:localhost}
@@ -18,27 +28,21 @@ spring:
prefer-ip-address: true prefer-ip-address: true
instance-id: ${spring.application.name}:${random.value} instance-id: ${spring.application.name}:${random.value}
# Spring Cloud Gateway 2025.0 — new prefix: spring.cloud.gateway.server.webflux
gateway: gateway:
server: server:
webflux: webflux:
# Trust Nginx reverse proxy for forwarded headers
trusted-proxies: 127\.0\.0\.1|10\.0\.0\..*|172\.1[6-9]\..*|172\.2[0-9]\..*|172\.3[0-1]\..*|192\.168\..* trusted-proxies: 127\.0\.0\.1|10\.0\.0\..*|172\.1[6-9]\..*|172\.2[0-9]\..*|172\.3[0-1]\..*|192\.168\..*
discovery: discovery:
locator: locator:
enabled: true enabled: true
lower-case-service-id: true lower-case-service-id: true
httpclient: httpclient:
connect-timeout: 5000 connect-timeout: 5000
response-timeout: 60s response-timeout: 60s
default-filters: default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
routes: routes:
# RAG Service - actuator (health, info) # RAG Service - actuator
- id: rag-service-actuator - id: rag-service-actuator
uri: lb://rag-service uri: lb://rag-service
predicates: predicates:
@@ -57,19 +61,29 @@ spring:
- RewritePath=/api/rag(?<segment>/?.*), ${segment} - RewritePath=/api/rag(?<segment>/?.*), ${segment}
- AddRequestHeader=X-Forwarded-Prefix, /api/rag - AddRequestHeader=X-Forwarded-Prefix, /api/rag
# Analytics Service (will be added later) # ---- JWT ----
# - id: analytics-service-api jwt:
# uri: lb://analytics-service secret: ${JWT_SECRET:}
# predicates: expiration: ${JWT_EXPIRATION:103600000}
# - Path=/api/analytics/**
# - Method=GET,POST
# filters:
# - RewritePath=/api/analytics(?<segment>/?.*), ${segment}
# ---- Auth path config ----
auth:
public-paths:
- /api/auth/login
- /api/auth/register
- /api/auth/refresh/token
- /actuator/**
- /api/*/v3/api-docs/**
- /api/*/swagger-ui/**
admin-paths:
- /api/*/admin/**
# ---- CORS ----
gateway: gateway:
cors: cors:
allowed-origins: ${CORS_ORIGINS:*} allowed-origins: ${CORS_ORIGINS:*}
# ---- Actuator ----
management: management:
endpoints: endpoints:
web: web:
@@ -81,8 +95,10 @@ management:
gateway: gateway:
enabled: true enabled: true
# ---- Logging ----
logging: logging:
level: level:
root: INFO root: INFO
com.posthub.gateway: DEBUG com.posthub.gateway: DEBUG
org.springframework.cloud.gateway: INFO org.springframework.cloud.gateway: INFO
org.springframework.r2dbc: INFO