auth media

This commit is contained in:
2026-03-14 22:15:17 +01:00
parent 895448945a
commit f661ef8918
9 changed files with 203 additions and 2 deletions

View File

@@ -1,5 +1,7 @@
package com.posthub.gateway.config;
import com.posthub.gateway.security.OAuth2AuthenticationSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -13,8 +15,11 @@ import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
@@ -27,7 +32,9 @@ public class SecurityConfig {
.pathMatchers(
"/api/auth/login",
"/api/auth/register",
"/api/auth/refresh/token"
"/api/auth/refresh/token",
"/oauth2/authorization/**",
"/login/oauth2/code/**"
).permitAll()
.pathMatchers(
"/actuator/**",
@@ -36,6 +43,9 @@ public class SecurityConfig {
).permitAll()
.anyExchange().permitAll()
)
.oauth2Login(oauth2 -> oauth2
.authenticationSuccessHandler(oAuth2AuthenticationSuccessHandler)
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((exchange, ex) -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);

View File

@@ -1,5 +1,6 @@
package com.posthub.gateway.model.entity;
import com.posthub.gateway.model.enums.AuthProvider;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import lombok.Getter;
@@ -40,4 +41,10 @@ public class User {
@Column("role")
private UserRole role;
@Column("auth_provider")
private AuthProvider authProvider;
@Column("provider_id")
private String providerId;
}

View File

@@ -0,0 +1,8 @@
package com.posthub.gateway.model.enums;
public enum AuthProvider {
LOCAL,
GOOGLE,
FACEBOOK,
GITHUB
}

View File

@@ -1,6 +1,7 @@
package com.posthub.gateway.repository;
import com.posthub.gateway.model.entity.User;
import com.posthub.gateway.model.enums.AuthProvider;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
@@ -11,4 +12,6 @@ public interface UserRepository extends ReactiveCrudRepository<User, Integer> {
Mono<User> findByUsername(String username);
Mono<Boolean> existsByEmail(String email);
Mono<User> findByAuthProviderAndProviderId(AuthProvider authProvider, String providerId);
}

View File

@@ -0,0 +1,135 @@
package com.posthub.gateway.security;
import com.posthub.gateway.model.entity.RefreshToken;
import com.posthub.gateway.model.entity.User;
import com.posthub.gateway.model.enums.AuthProvider;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import com.posthub.gateway.repository.RefreshTokenRepository;
import com.posthub.gateway.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.UUID;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtTokenProvider jwtTokenProvider;
@Value("${oauth2.redirect-uri:https://balexvic.com/login}")
private String redirectUri;
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
OAuth2User oAuth2User = authToken.getPrincipal();
String registrationId = authToken.getAuthorizedClientRegistrationId();
AuthProvider provider = AuthProvider.valueOf(registrationId.toUpperCase());
String providerId = extractProviderId(oAuth2User, registrationId);
String email = extractEmail(oAuth2User, registrationId);
String name = extractName(oAuth2User, registrationId);
return userRepository.findByAuthProviderAndProviderId(provider, providerId)
.switchIfEmpty(
email != null
? userRepository.findByEmail(email)
.flatMap(existingUser -> {
existingUser.setAuthProvider(provider);
existingUser.setProviderId(providerId);
existingUser.setUpdated(LocalDateTime.now());
return userRepository.save(existingUser);
})
.switchIfEmpty(Mono.defer(() -> createNewUser(provider, providerId, email, name)))
: Mono.defer(() -> createNewUser(provider, providerId, email, name))
)
.flatMap(user -> {
user.setLastLogin(LocalDateTime.now());
user.setUpdated(LocalDateTime.now());
return userRepository.save(user);
})
.flatMap(this::generateTokensAndRedirect)
.flatMap(redirectUrl -> {
webFilterExchange.getExchange().getResponse().setStatusCode(HttpStatus.FOUND);
webFilterExchange.getExchange().getResponse().getHeaders().setLocation(URI.create(redirectUrl));
return webFilterExchange.getExchange().getResponse().setComplete();
});
}
private Mono<User> createNewUser(AuthProvider provider, String providerId, String email, String name) {
User user = new User();
user.setAuthProvider(provider);
user.setProviderId(providerId);
user.setEmail(email);
user.setUsername(name != null ? name : "user_" + providerId.substring(0, 8));
user.setRegistrationStatus(RegistrationStatus.ACTIVE);
user.setRole(UserRole.USER);
user.setCreated(LocalDateTime.now());
user.setUpdated(LocalDateTime.now());
user.setDeleted(false);
log.info("Creating new OAuth2 user: provider={}, email={}", provider, email);
return userRepository.save(user);
}
private Mono<String> generateTokensAndRedirect(User user) {
return refreshTokenRepository.findByUserId(user.getId())
.flatMap(existing -> {
existing.setCreated(LocalDateTime.now());
existing.setToken(generateUuid());
existing.setSessionId(generateUuid());
return refreshTokenRepository.save(existing);
})
.switchIfEmpty(Mono.defer(() -> {
RefreshToken newToken = new RefreshToken();
newToken.setUserId(user.getId());
newToken.setCreated(LocalDateTime.now());
newToken.setToken(generateUuid());
newToken.setSessionId(generateUuid());
return refreshTokenRepository.save(newToken);
}))
.map(refreshToken -> {
String accessToken = jwtTokenProvider.generateToken(user, refreshToken.getSessionId());
return redirectUri + "?token=" + accessToken + "&refreshToken=" + refreshToken.getToken();
});
}
private String extractProviderId(OAuth2User oAuth2User, String registrationId) {
return switch (registrationId) {
case "google" -> oAuth2User.getAttribute("sub");
case "facebook" -> oAuth2User.getAttribute("id");
case "github" -> String.valueOf(oAuth2User.getAttributes().get("id"));
default -> oAuth2User.getName();
};
}
private String extractEmail(OAuth2User oAuth2User, String registrationId) {
return oAuth2User.getAttribute("email");
}
private String extractName(OAuth2User oAuth2User, String registrationId) {
return switch (registrationId) {
case "github" -> oAuth2User.getAttribute("login");
default -> oAuth2User.getAttribute("name");
};
}
private String generateUuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}

View File

@@ -6,6 +6,23 @@ spring:
application:
name: gateway-service
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:}
client-secret: ${GOOGLE_CLIENT_SECRET:}
scope: email,profile
github:
client-id: ${GITHUB_CLIENT_ID:}
client-secret: ${GITHUB_CLIENT_SECRET:}
scope: user:email,read:user
facebook:
client-id: ${FACEBOOK_CLIENT_ID:}
client-secret: ${FACEBOOK_CLIENT_SECRET:}
scope: email,public_profile
# ---- R2DBC (reactive DB) ----
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
@@ -75,12 +92,18 @@ jwt:
secret: ${JWT_SECRET:}
expiration: ${JWT_EXPIRATION:103600000}
# ---- OAuth2 redirect after success ----
oauth2:
redirect-uri: ${OAUTH2_REDIRECT_URI:https://balexvic.com/login}
# ---- Auth path config ----
auth:
public-paths:
- /api/auth/login
- /api/auth/register
- /api/auth/refresh/token
- /oauth2/authorization/**
- /login/oauth2/code/**
- /actuator/**
- /api/*/v3/api-docs/**
- /api/*/swagger-ui/**
@@ -102,7 +125,7 @@ management:
health:
show-details: always
gateway:
enabled: true
access: unrestricted
# ---- Logging ----
logging: