Add SPA sample using BFF and Spring Cloud Gateway
This commit is contained in:
1
samples/backend-for-spa-client/gradle.properties
Normal file
1
samples/backend-for-spa-client/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring-security.version=6.3.0
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "3.2.2"
|
||||
id "io.spring.dependency-management" version "1.1.0"
|
||||
id "java"
|
||||
}
|
||||
|
||||
group = project.rootProject.group
|
||||
version = project.rootProject.version
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
}
|
||||
|
||||
ext {
|
||||
set("springCloudVersion", "2023.0.2")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
|
||||
implementation "org.springframework.cloud:spring-cloud-starter-gateway-mvc"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 1.4
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class BackendForSpaClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BackendForSpaClientApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 1.4
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class CorsConfig {
|
||||
|
||||
@Value("${app.base-uri}")
|
||||
private String appBaseUri;
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedHeader("X-XSRF-TOKEN");
|
||||
config.addAllowedHeader(HttpHeaders.CONTENT_TYPE);
|
||||
config.setAllowedMethods(Arrays.asList("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedOrigins(Collections.singletonList(this.appBaseUri));
|
||||
config.setAllowCredentials(true);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.web.servlet.function.HandlerFilterFunction;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.getApplicationContext;
|
||||
|
||||
/**
|
||||
* Custom {@code HandlerFilterFunction}'s registered in META-INF/spring.factories and used in application.yml.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.4
|
||||
*/
|
||||
public interface GatewayFilterFunctions {
|
||||
|
||||
@Shortcut
|
||||
static HandlerFilterFunction<ServerResponse, ServerResponse> relayTokenIfExists(String clientRegistrationId) {
|
||||
return (request, next) -> {
|
||||
Authentication principal = (Authentication) request.servletRequest().getUserPrincipal();
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = getApplicationContext(request)
|
||||
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||
OAuth2AuthorizedClient authorizedClient = authorizedClientRepository.loadAuthorizedClient(
|
||||
clientRegistrationId, principal, request.servletRequest());
|
||||
if (authorizedClient != null) {
|
||||
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
|
||||
ServerRequest bearerRequest = ServerRequest.from(request)
|
||||
.headers(httpHeaders -> httpHeaders.setBearerAuth(accessToken.getTokenValue())).build();
|
||||
return next.handle(bearerRequest);
|
||||
}
|
||||
return next.handle(request);
|
||||
};
|
||||
}
|
||||
|
||||
class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
FilterSupplier() {
|
||||
super(GatewayFilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 1.4
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Value("${app.base-uri}")
|
||||
private String appBaseUri;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
|
||||
/*
|
||||
IMPORTANT:
|
||||
Set the csrfRequestAttributeName to null, to opt-out of deferred tokens, resulting in the CsrfToken to be loaded on every request.
|
||||
If it does not exist, the CookieCsrfTokenRepository will automatically generate a new one and add the Cookie to the response.
|
||||
See the reference: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#deferred-csrf-token
|
||||
*/
|
||||
csrfTokenRequestAttributeHandler.setCsrfRequestAttributeName(null);
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests(authorize ->
|
||||
authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf ->
|
||||
csrf
|
||||
.csrfTokenRepository(cookieCsrfTokenRepository)
|
||||
.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)
|
||||
)
|
||||
.cors(Customizer.withDefaults())
|
||||
.exceptionHandling(exceptionHandling ->
|
||||
exceptionHandling
|
||||
.authenticationEntryPoint(authenticationEntryPoint())
|
||||
)
|
||||
.oauth2Login(oauth2Login ->
|
||||
oauth2Login
|
||||
.successHandler(new SimpleUrlAuthenticationSuccessHandler(this.appBaseUri)))
|
||||
.logout(logout ->
|
||||
logout
|
||||
.addLogoutHandler(logoutHandler(cookieCsrfTokenRepository))
|
||||
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
|
||||
)
|
||||
.oauth2Client(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private AuthenticationEntryPoint authenticationEntryPoint() {
|
||||
AuthenticationEntryPoint authenticationEntryPoint =
|
||||
new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/messaging-client-oidc");
|
||||
MediaTypeRequestMatcher textHtmlMatcher =
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML);
|
||||
textHtmlMatcher.setUseEquals(true);
|
||||
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
|
||||
entryPoints.put(textHtmlMatcher, authenticationEntryPoint);
|
||||
|
||||
DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
|
||||
delegatingAuthenticationEntryPoint.setDefaultEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
|
||||
return delegatingAuthenticationEntryPoint;
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(CsrfTokenRepository csrfTokenRepository) {
|
||||
return new CompositeLogoutHandler(
|
||||
new SecurityContextLogoutHandler(),
|
||||
new CsrfLogoutHandler(csrfTokenRepository));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.web;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 1.4
|
||||
*/
|
||||
@Controller
|
||||
public class DefaultController {
|
||||
|
||||
@Value("${app.base-uri}")
|
||||
private String appBaseUri;
|
||||
|
||||
@GetMapping("/")
|
||||
public String root() {
|
||||
return "redirect:" + this.appBaseUri;
|
||||
}
|
||||
|
||||
// '/authorized' is the registered 'redirect_uri' for authorization_code
|
||||
@GetMapping("/authorized")
|
||||
public String authorized() {
|
||||
return "redirect:" + this.appBaseUri;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
|
||||
sample.config.GatewayFilterFunctions.FilterSupplier
|
||||
@@ -0,0 +1,53 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.springframework.web: INFO
|
||||
org.springframework.security: INFO
|
||||
org.springframework.security.oauth2: INFO
|
||||
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
messaging-client-oidc:
|
||||
provider: spring
|
||||
client-id: messaging-client
|
||||
client-secret: secret
|
||||
authorization-grant-type: authorization_code
|
||||
redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
|
||||
scope: openid,profile
|
||||
client-name: messaging-client-oidc
|
||||
messaging-client-authorization-code:
|
||||
provider: spring
|
||||
client-id: messaging-client
|
||||
client-secret: secret
|
||||
authorization-grant-type: authorization_code
|
||||
redirect-uri: "http://127.0.0.1:8080/authorized"
|
||||
scope: message.read,message.write
|
||||
client-name: messaging-client-authorization-code
|
||||
provider:
|
||||
spring:
|
||||
issuer-uri: http://localhost:9000
|
||||
cloud:
|
||||
gateway:
|
||||
mvc:
|
||||
routes:
|
||||
- id: userinfo
|
||||
uri: http://localhost:9000
|
||||
predicates:
|
||||
- Path=/userinfo
|
||||
filters:
|
||||
- TokenRelay=
|
||||
- id: messages
|
||||
uri: http://localhost:8090
|
||||
predicates:
|
||||
- Path=/messages
|
||||
filters:
|
||||
- RelayTokenIfExists=messaging-client-authorization-code
|
||||
|
||||
app:
|
||||
base-uri: http://127.0.0.1:4200
|
||||
Reference in New Issue
Block a user