auth gateway
This commit is contained in:
@@ -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<>();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.posthub.gateway.model.enums;
|
||||
|
||||
public enum RegistrationStatus {
|
||||
ACTIVE,
|
||||
BLOCKED,
|
||||
DELETED
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.posthub.gateway.model.enums;
|
||||
|
||||
public enum UserRole {
|
||||
USER,
|
||||
ADMIN
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,16 @@ spring:
|
||||
application:
|
||||
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:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
@@ -18,27 +28,21 @@ spring:
|
||||
prefer-ip-address: true
|
||||
instance-id: ${spring.application.name}:${random.value}
|
||||
|
||||
# Spring Cloud Gateway 2025.0 — new prefix: spring.cloud.gateway.server.webflux
|
||||
gateway:
|
||||
server:
|
||||
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\..*
|
||||
|
||||
discovery:
|
||||
locator:
|
||||
enabled: true
|
||||
lower-case-service-id: true
|
||||
|
||||
httpclient:
|
||||
connect-timeout: 5000
|
||||
response-timeout: 60s
|
||||
|
||||
default-filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
|
||||
|
||||
routes:
|
||||
# RAG Service - actuator (health, info)
|
||||
# RAG Service - actuator
|
||||
- id: rag-service-actuator
|
||||
uri: lb://rag-service
|
||||
predicates:
|
||||
@@ -57,19 +61,29 @@ spring:
|
||||
- RewritePath=/api/rag(?<segment>/?.*), ${segment}
|
||||
- AddRequestHeader=X-Forwarded-Prefix, /api/rag
|
||||
|
||||
# Analytics Service (will be added later)
|
||||
# - id: analytics-service-api
|
||||
# uri: lb://analytics-service
|
||||
# predicates:
|
||||
# - Path=/api/analytics/**
|
||||
# - Method=GET,POST
|
||||
# filters:
|
||||
# - RewritePath=/api/analytics(?<segment>/?.*), ${segment}
|
||||
# ---- JWT ----
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
expiration: ${JWT_EXPIRATION:103600000}
|
||||
|
||||
# ---- 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:
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS:*}
|
||||
|
||||
# ---- Actuator ----
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@@ -81,8 +95,10 @@ management:
|
||||
gateway:
|
||||
enabled: true
|
||||
|
||||
# ---- Logging ----
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.posthub.gateway: DEBUG
|
||||
org.springframework.cloud.gateway: INFO
|
||||
org.springframework.r2dbc: INFO
|
||||
Reference in New Issue
Block a user