auth media
This commit is contained in:
@@ -106,6 +106,13 @@
|
||||
<version>${lombok.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- OAuth2 Client (social login) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.posthub.gateway.model.enums;
|
||||
|
||||
public enum AuthProvider {
|
||||
LOCAL,
|
||||
GOOGLE,
|
||||
FACEBOOK,
|
||||
GITHUB
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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("-", "");
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user