diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README-OAuth.md b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README-OAuth.md new file mode 100644 index 0000000000..c186114589 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README-OAuth.md @@ -0,0 +1,40 @@ +# OAuth Test Setup + +In order to test the OAuth-secured gateway configurations, please follow the steps below + +## Keycloak setup + +1. Clone or download the https://github.com/Baeldung/spring-security-oauth project +2. Replace the file `oauth-rest/oauth-authorization-server/src/main/resources/baeldung-realm.json` + with the one provider here +3. Go to the oauth-rest/oauth-authorization-server folder and use maven to build the project +4. Run the Keycloack service with `mvn spring-boot:run` +5. Once Keycloak is up and running, go to `http://localhost:8083/auth/admin/master/console/#/realms/baeldung` and + log in with using `bael-admin/pass` as credentials +6. Create two test users, so that one belongs to the *Golden Customers* group and the other doesn't. + +## Quotes backend + +Use the provided maven profile: + +``` +$ mvn spring-boot:run -Pquotes-application +``` + +## Gateway as Resource Server + +Use the provided maven profile: + +``` +$ mvn spring-boot:run -Pgateway-as-resource-server +``` + +## Gateway as OAuth 2.0 Client + +Use the provided maven profile: + +``` +$ mvn spring-boot:run -Pgateway-as-oauth-client +``` + + diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README.md b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README.md new file mode 100644 index 0000000000..80315040c9 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/README.md @@ -0,0 +1,13 @@ +## Spring Cloud Gateway + +This module contains articles about Spring Cloud Gateway + +### Relevant Articles: + +- [Exploring the New Spring Cloud Gateway](http://www.baeldung.com/spring-cloud-gateway) +- [Writing Custom Spring Cloud Gateway Filters](https://www.baeldung.com/spring-cloud-custom-gateway-filters) +- [Spring Cloud Gateway Routing Predicate Factories](https://www.baeldung.com/spring-cloud-gateway-routing-predicate-factories) +- [Spring Cloud Gateway WebFilter Factories](https://www.baeldung.com/spring-cloud-gateway-webfilter-factories) +- [Using Spring Cloud Gateway with OAuth 2.0 Patterns](https://www.baeldung.com/spring-cloud-gateway-oauth2) +- [URL Rewriting With Spring Cloud Gateway](https://www.baeldung.com/spring-cloud-gateway-url-rewriting) +- [Processing the Response Body in Spring Cloud Gateway](https://www.baeldung.com/spring-cloud-gateway-response-body) diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/baeldung-realm.json b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/baeldung-realm.json new file mode 100644 index 0000000000..4dad262568 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/baeldung-realm.json @@ -0,0 +1,2186 @@ +{ + "id": "baeldung", + "realm": "baeldung", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "2f721e46-398b-43ff-845f-3c747f1f69f5", + "name": "standard", + "description": "Standard customer", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + }, + { + "id": "b8b1cb49-c953-4cb0-acea-62b24c4b6d30", + "name": "silver", + "description": "Silver customer", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + }, + { + "id": "f44f5ba9-2393-4d2a-b581-81cf8dd7f245", + "name": "gold", + "description": "Gold customers", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + }, + { + "id": "3b6109f5-6e5a-4578-83c3-791ec3e2bf9e", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + }, + { + "id": "0dd6a8c7-d669-4941-9ea1-521980e9c53f", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + }, + { + "id": "ca962095-7f9b-49e2-a190-e391a0d4b704", + "name": "user", + "composite": false, + "clientRole": false, + "containerId": "baeldung", + "attributes": {} + } + ], + "client": { + "newClient": [], + "realm-management": [ + { + "id": "5d00243f-ceec-4b0c-995e-d86d5b8a0ae6", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "941612de-bd85-47a5-8dfa-37c270dde28c", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "5ea9810d-63cc-4277-9b32-ba8a3d3c6091", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "8b7b0dd8-350b-473e-b8cd-8acad34f1358", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "0f8e5ee8-b014-4b7c-9b69-50f46abcba5f", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "911b1489-9383-4734-b134-bf49bf992ce9", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "5d48274c-bd6b-4c26-ad54-f1a2254beac0", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "3ea43b64-316f-4693-8346-9ee78b24adaf", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "49735614-96ec-49b2-98fe-3af9bcd1a33a", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "e8f8c3cc-0ff1-4f72-a271-db6821a3cdb6", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "387418b1-4f80-4b00-b9dd-805ca041f805", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "427c27d4-521a-464b-a0df-16d7f537e8d5", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "view-clients", + "view-authorization", + "query-groups", + "manage-realm", + "query-clients", + "manage-clients", + "view-realm", + "manage-identity-providers", + "create-client", + "manage-users", + "view-identity-providers", + "query-users", + "query-realms", + "view-users", + "impersonation", + "manage-authorization", + "manage-events", + "view-events" + ] + } + }, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "a574cf01-03e4-4573-ab9e-276d13a1ce8d", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "c3a253a8-a1b6-4d38-9677-f728f32482ad", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "f3cb93da-273e-419a-b2f4-93f09896abcf", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "6eedf2b7-50ef-4495-a89b-54aef751b7fa", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "0332e99b-3dfc-4193-9e13-5728f8f3e6d6", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "b690cb9c-0f4a-4be5-ade0-b40443d8149d", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + }, + { + "id": "aac3def5-f193-4a6c-9065-1667a0746a8a", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "6a4bfbd0-576d-4778-af56-56f876647355", + "attributes": {} + } + ], + "security-admin-console": [], + "quotes-client": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "397b5703-4c81-48fd-a24c-a7e8177ef657", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "4b9609f0-48d1-4e71-9381-2ecec08616f9", + "attributes": {} + } + ], + "account": [ + { + "id": "8daa8096-d14e-4d1c-ad1f-83f822016aa1", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + }, + { + "id": "15f0d8be-932b-4565-8ad0-e8aa170093dd", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + }, + { + "id": "c5aa697c-bf87-47c6-bd94-9121b72420b9", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + }, + { + "id": "f0b3bbe4-aec1-4227-b9d3-2c314d612a04", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + }, + { + "id": "948269c7-a69c-4c82-a7f3-88868713dfd9", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + }, + { + "id": "aed18201-2433-4998-8fa3-0979b0b31c10", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "635c3314-f15f-4c02-bcb7-8739fd4c21b7", + "name": "golden_customers", + "path": "/golden_customers", + "attributes": {}, + "realmRoles": [ + "gold" + ], + "clientRoles": {}, + "subGroups": [] + }, + { + "id": "279c5ec4-0588-4884-91c1-2697ed5c9826", + "name": "silver_customers", + "path": "/silver_customers", + "attributes": {}, + "realmRoles": [ + "silver" + ], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultRoles": [ + "uma_authorization", + "offline_access" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/baeldung/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "defaultRoles": [ + "manage-account", + "view-profile" + ], + "redirectUris": [ + "/realms/baeldung/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8209784b-8540-43c2-aece-241acf12ea5a", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/baeldung/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/realms/baeldung/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "58395b96-1718-4787-8f92-74577e2bfc30", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "13d76feb-d762-4409-bb84-7a75bc395a61", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "4b9609f0-48d1-4e71-9381-2ecec08616f9", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b88ce206-63d6-43b6-87c9-ea09d8c02f32", + "clientId": "newClient", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost:8082/new-client/login/oauth2/code/custom", + "http://localhost:8089/", + "http://localhost:8089/auth/redirect/" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "role_list", + "profile" + ], + "optionalClientScopes": [ + "web-origins", + "address", + "read", + "phone", + "roles", + "offline_access", + "microprofile-jwt", + "write", + "email" + ] + }, + { + "id": "5898f71f-b91e-4c3f-9c86-48de0e8665c4", + "clientId": "quotes-client", + "name": "Quotes Client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "0e082231-a70d-48e8-b8a5-fbfb743041b6", + "redirectUris": [ + "http://localhost:8082/*", + "http://localhost:8087/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.multivalued.roles": "false", + "saml.force.post.binding": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "008d32b5-ea7b-4739-89af-e90fe137bda9", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6a4bfbd0-576d-4778-af56-56f876647355", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8e358d2f-b085-4243-8e6e-c175431e5eeb", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/baeldung/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/admin/baeldung/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9cfca9ee-493d-4b5e-8170-2d364149de59", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "77c7e29d-1a22-4419-bbfb-4a62bb033449", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "94e1879d-b49e-4178-96e0-bf8d7f32c160", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "b3526ac1-10e2-4344-8621-9c5a0853e97a", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d30270dc-baa6-455a-8ff6-ddccf8a78d86", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "f5b1684d-e479-4134-8578-457fa64717da", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "c658ae14-e96a-4745-b21b-2ed5c4c63f5f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "959521bc-5ffd-465b-95f2-5b0c20d1909c", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "07b8550c-b298-4cce-9ffb-900182575b76", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "569b3d44-4ecd-4768-a58c-70ff38f4b4fe", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "a3e7b19d-df6c-437e-9eea-06fec1becb2f", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "72a070f7-4363-4c88-8153-6fd2d12b9b04", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "24b42c6d-a93c-4aa1-9a03-2a2b55954c13", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ba8c9950-fd0b-4434-8be6-b58456d7b6d4", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0a9ddd71-309c-40f0-8ea6-a0791070c6ed", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "fbf53bbd-1ad0-4bf8-8030-50f81696d8ee", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "423be2cd-42c0-462e-9030-18f9b28ff2d3", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "53eb9006-4b81-474a-8b60-80f775d54b63", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "4d8bc82a-eaeb-499e-8eb2-0f1dcbe91699", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "d3b25485-4042-419d-afff-cfd63a76e229", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "422cfa5a-f2f4-4f36-82df-91b47ae1ea50", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "3f2863c1-d98d-45b5-b08f-af9c4d9c10f8", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "c98c063d-eee4-41a0-9130-595afd709d1f", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "8dbed80a-d672-4185-8dda-4bba2a56ec83", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "5e5c690c-93cf-489d-a054-b109eab8911b", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "3b985202-af8a-42f1-ac5f-0966a404f5d7", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "6eafd1b3-7121-4919-ad1e-039fa58acc32", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "73cba925-8c31-443f-9601-b1514e6396c1", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "c1a2eb23-25c6-4be7-a791-bbdca99c83f7", + "name": "read", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "id": "18e141bf-dabe-4858-879c-dbc439cdead4", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "10cbe37f-0198-4d65-bc8a-bfe5ad8145d1", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "111ed87a-5fd3-4cee-96df-8dbfb88cfdc0", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "24924d8d-6071-4a93-b40f-326176cb335e", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "2f6a9bdf-3758-484c-996d-e4f93555559f", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "804d4798-d9a3-4fd3-8b28-d12142e8cb3d", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "51d49314-b511-43e0-9258-bfb873758a78", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "2b384cd0-9e85-4a87-8eeb-2b480b0587b7", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "c3e253fb-7361-47cf-9d4a-86245686fdf1", + "name": "write", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + } + ], + "defaultDefaultClientScopes": [ + "roles", + "role_list", + "web-origins", + "email", + "profile" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "84305f42-4b6d-4b0a-ac7c-53e406e3ac63", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "c7c38a95-744f-4558-a403-9cf692fe1944", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "365b2899-befe-4417-b89b-562650ec4446", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "81c32244-7921-43e9-9356-a3469259b78c", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "d09b2147-afea-4f7f-a49c-0aec7eee10de", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "41ffde1b-72a2-416f-87a7-94989e940dc0", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "76075388-2782-4656-a986-313493239a9f", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "3caaf57a-9cd7-48c1-b709-b40b887414f7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "d67a940a-52e4-44a5-9f69-6ffdd67a188f", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "48d40de3-6234-42e8-9449-f68f56abb54b", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "52ea1c5d-2a30-459f-b66a-249f298b32f8", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "b35b726e-c1cc-4a31-8670-8c858c088498", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Handle Existing Account - Alternatives - 0", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "b7cb4b8c-0064-4c6b-95ee-d7f50011bae7", + "alias": "Handle Existing Account - Alternatives - 0", + "description": "Subflow of Handle Existing Account with alternative executions", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "2576bf21-a516-4e22-8ed8-8a1a3d35c06a", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "867b0355-c2b0-4f4e-b535-09406e2bc086", + "alias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional", + "description": "Flow to determine if the auth-otp-form authenticator should be used or not.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "52e45a6a-292f-4a34-9c02-7f97d9997a9c", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ef818d24-fb06-418a-a093-16239068cb58", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "f7864004-be4b-40f2-8534-454981f09f98", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "direct grant - direct-grant-validate-otp - Conditional", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "233225f2-78b0-4756-9568-a775ea7cc975", + "alias": "direct grant - direct-grant-validate-otp - Conditional", + "description": "Flow to determine if the direct-grant-validate-otp authenticator should be used or not.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "2787939c-3312-4d56-9d61-22a71947e154", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c80ca473-6e65-4140-a9d1-035414546bfb", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "first broker login - Alternatives - 0", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "700a71e1-f176-432e-9e70-ccb06d539730", + "alias": "first broker login - Alternatives - 0", + "description": "Subflow of first broker login with alternative executions", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "13cd2ee9-8ebb-4651-8a9d-535d4020d889", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "forms - auth-otp-form - Conditional", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "80067ebf-72b9-4ba5-abc3-161dfdd53938", + "alias": "forms - auth-otp-form - Conditional", + "description": "Flow to determine if the auth-otp-form authenticator should be used or not.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "269354b1-7dd7-46ff-8a69-084cd2c7be80", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c5fa64a0-e784-490f-8879-0e8a209e3636", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "bc48f897-4d16-49a8-be70-183c7867e20a", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "fddaba41-87b1-40d1-b147-f062c38e39ad", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "reset credentials - reset-otp - Conditional", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6b9fa295-1eb4-48b0-a286-b17ebd6be7e1", + "alias": "reset credentials - reset-otp - Conditional", + "description": "Flow to determine if the reset-otp authenticator should be used or not.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c890d4c4-e4e2-4618-b69d-8d30f8278965", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "2b02fbe4-e05e-4238-bb0e-04da7d4efd4e", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "07dd933b-ce95-4a7c-bd81-3efa24f9558c", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0" + }, + "keycloakVersion": "11.0.2", + "userManagedAccessAllowed": false +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/pom.xml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/pom.xml new file mode 100644 index 0000000000..dc42483dc5 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + gateway-2 + spring-cloud-gateway + jar + + + com.baeldung.spring.cloud + spring-cloud-modules + 1.0.0-SNAPSHOT + + + + + + org.junit + junit-bom + ${junit-jupiter.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + it.ozimov + embedded-redis + ${redis.version} + test + + + org.hibernate + hibernate-validator-cdi + ${hibernate-validator.version} + + + javax.validation + validation-api + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-devtools + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + + quotes-application + + + + org.springframework.boot + spring-boot-maven-plugin + + com.baeldung.springcloudgateway.oauth.backend.QuotesApplication + + + + + + + gateway-as-resource-server + + + + org.springframework.boot + spring-boot-maven-plugin + + com.baeldung.springcloudgateway.oauth.server.ResourceServerGatewayApplication + -Dspring.profiles.active=resource-server + + + + + + + gateway-as-oauth-client + + + + org.springframework.boot + spring-boot-maven-plugin + + com.baeldung.springcloudgateway.oauth.server.ResourceServerGatewayApplication + -Dspring.profiles.active=oauth-client + + + + + + + gateway-url-rewrite + + + + org.springframework.boot + spring-boot-maven-plugin + + com.baeldung.springcloudgateway.rewrite.URLRewriteGatewayApplication + -Dspring.profiles.active=url-rewrite + + + + + + + + + + + 6.0.2.Final + 0.7.2 + 9.19 + + + + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersGatewayApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersGatewayApplication.java new file mode 100644 index 0000000000..fae25bb7cc --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersGatewayApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:customfilters-global-application.properties") +public class CustomFiltersGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(CustomFiltersGatewayApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/config/WebClientConfig.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/config/WebClientConfig.java new file mode 100644 index 0000000000..0589d8c321 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/config/WebClientConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + WebClient client() { + return WebClient.builder() + .build(); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ChainRequestGatewayFilterFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ChainRequestGatewayFilterFactory.java new file mode 100644 index 0000000000..f53b0a3c93 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ChainRequestGatewayFilterFactory.java @@ -0,0 +1,89 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale.LanguageRange; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.core.publisher.Mono; + +@Component +public class ChainRequestGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ChainRequestGatewayFilterFactory.class); + + private final WebClient client; + + public ChainRequestGatewayFilterFactory(WebClient client) { + super(Config.class); + this.client = client; + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("languageServiceEndpoint", "defaultLanguage"); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + return client.get() + .uri(config.getLanguageServiceEndpoint()) + .exchange() + .flatMap(response -> { + return (response.statusCode() + .is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage()); + }) + .map(LanguageRange::parse) + .map(range -> { + exchange.getRequest() + .mutate() + .headers(h -> h.setAcceptLanguage(range)); + + String allOutgoingRequestLanguages = exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .stream() + .map(r -> r.getRange()) + .collect(Collectors.joining(",")); + + logger.info("Chain Request output - Request contains Accept-Language header: " + allOutgoingRequestLanguages); + + return exchange; + }) + .flatMap(chain::filter); + + }; + } + + public static class Config { + private String languageServiceEndpoint; + private String defaultLanguage; + + public Config() { + } + + public String getLanguageServiceEndpoint() { + return languageServiceEndpoint; + } + + public void setLanguageServiceEndpoint(String languageServiceEndpoint) { + this.languageServiceEndpoint = languageServiceEndpoint; + } + + public String getDefaultLanguage() { + return defaultLanguage; + } + + public void setDefaultLanguage(String defaultLanguage) { + this.defaultLanguage = defaultLanguage; + } + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/LoggingGatewayFilterFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/LoggingGatewayFilterFactory.java new file mode 100644 index 0000000000..900d36cc02 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/LoggingGatewayFilterFactory.java @@ -0,0 +1,85 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Mono; + +@Component +public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class); + + public static final String BASE_MSG = "baseMessage"; + public static final String PRE_LOGGER = "preLogger"; + public static final String POST_LOGGER = "postLogger"; + + public LoggingGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(BASE_MSG, PRE_LOGGER, POST_LOGGER); + } + + @Override + public GatewayFilter apply(Config config) { + return new OrderedGatewayFilter((exchange, chain) -> { + if (config.isPreLogger()) + logger.info("Pre GatewayFilter logging: " + config.getBaseMessage()); + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + if (config.isPostLogger()) + logger.info("Post GatewayFilter logging: " + config.getBaseMessage()); + })); + }, 1); + } + + public static class Config { + private String baseMessage; + private boolean preLogger; + private boolean postLogger; + + public Config() { + }; + + public Config(String baseMessage, boolean preLogger, boolean postLogger) { + super(); + this.baseMessage = baseMessage; + this.preLogger = preLogger; + this.postLogger = postLogger; + } + + public String getBaseMessage() { + return this.baseMessage; + } + + public boolean isPreLogger() { + return preLogger; + } + + public boolean isPostLogger() { + return postLogger; + } + + public void setBaseMessage(String baseMessage) { + this.baseMessage = baseMessage; + } + + public void setPreLogger(boolean preLogger) { + this.preLogger = preLogger; + } + + public void setPostLogger(boolean postLogger) { + this.postLogger = postLogger; + } + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyRequestGatewayFilterFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyRequestGatewayFilterFactory.java new file mode 100644 index 0000000000..5828f35a36 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyRequestGatewayFilterFactory.java @@ -0,0 +1,92 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class ModifyRequestGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ModifyRequestGatewayFilterFactory.class); + + public ModifyRequestGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("defaultLocale"); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + if (exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .isEmpty()) { + + String queryParamLocale = exchange.getRequest() + .getQueryParams() + .getFirst("locale"); + + Locale requestLocale = Optional.ofNullable(queryParamLocale) + .map(l -> Locale.forLanguageTag(l)) + .orElse(config.getDefaultLocale()); + + exchange.getRequest() + .mutate() + .headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale))); + } + + String allOutgoingRequestLanguages = exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .stream() + .map(range -> range.getRange()) + .collect(Collectors.joining(",")); + + logger.info("Modify request output - Request contains Accept-Language header: {}", allOutgoingRequestLanguages); + + ServerWebExchange modifiedExchange = exchange.mutate() + .request(originalRequest -> originalRequest.uri(UriComponentsBuilder.fromUri(exchange.getRequest() + .getURI()) + .replaceQueryParams(new LinkedMultiValueMap()) + .build() + .toUri())) + .build(); + + logger.info("Removed all query params: {}", modifiedExchange.getRequest() + .getURI()); + + return chain.filter(modifiedExchange); + }; + } + + public static class Config { + private Locale defaultLocale; + + public Config() { + } + + public Locale getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = Locale.forLanguageTag(defaultLocale); + }; + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyResponseGatewayFilterFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyResponseGatewayFilterFactory.java new file mode 100644 index 0000000000..f5efa69402 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ModifyResponseGatewayFilterFactory.java @@ -0,0 +1,48 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Mono; + +@Component +public class ModifyResponseGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ModifyResponseGatewayFilterFactory.class); + + public ModifyResponseGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + ServerHttpResponse response = exchange.getResponse(); + + Optional.ofNullable(exchange.getRequest() + .getQueryParams() + .getFirst("locale")) + .ifPresent(qp -> { + String responseContentLanguage = response.getHeaders() + .getContentLanguage() + .getLanguage(); + + response.getHeaders() + .add("Bael-Custom-Language-Header", responseContentLanguage); + logger.info("Added custom header to Response"); + }); + })); + }; + } + + public static class Config { + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactory.java new file mode 100644 index 0000000000..dbe9a9fb4f --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactory.java @@ -0,0 +1,110 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import reactor.core.publisher.Mono; + +@Component +public class ScrubResponseGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ScrubResponseGatewayFilterFactory.class); + private ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory; + + public ScrubResponseGatewayFilterFactory(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) { + super(Config.class); + this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory; + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("fields", "replacement"); + } + + + @Override + public GatewayFilter apply(Config config) { + + return modifyResponseBodyFilterFactory + .apply(c -> c.setRewriteFunction(JsonNode.class, JsonNode.class, new Scrubber(config))); + } + + public static class Config { + + private String fields; + private String replacement; + + + public String getFields() { + return fields; + } + public void setFields(String fields) { + this.fields = fields; + } + public String getReplacement() { + return replacement; + } + public void setReplacement(String replacement) { + this.replacement = replacement; + } + } + + + public static class Scrubber implements RewriteFunction { + private final Pattern fields; + private final String replacement; + + public Scrubber(Config config) { + this.fields = Pattern.compile(config.getFields()); + this.replacement = config.getReplacement(); + } + + @Override + public Publisher apply(ServerWebExchange t, JsonNode u) { + return Mono.just(scrubRecursively(u)); + } + + private JsonNode scrubRecursively(JsonNode u) { + if ( !u.isContainerNode()) { + return u; + } + + if ( u.isObject()) { + ObjectNode node = (ObjectNode)u; + node.fields().forEachRemaining((f) -> { + if ( fields.matcher(f.getKey()).matches() && f.getValue().isTextual()) { + f.setValue(TextNode.valueOf(replacement)); + } + else { + f.setValue(scrubRecursively(f.getValue())); + } + }); + } + else if ( u.isArray()) { + ArrayNode array = (ArrayNode)u; + for ( int i = 0 ; i < array.size() ; i++ ) { + array.set(i, scrubRecursively(array.get(i))); + } + } + + return u; + } + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/FirstPreLastPostGlobalFilter.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/FirstPreLastPostGlobalFilter.java new file mode 100644 index 0000000000..687f3fe685 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/FirstPreLastPostGlobalFilter.java @@ -0,0 +1,31 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +@Component +public class FirstPreLastPostGlobalFilter implements GlobalFilter, Ordered { + + final Logger logger = LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + logger.info("First Pre Global Filter"); + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + logger.info("Last Post Global Filter"); + })); + } + + @Override + public int getOrder() { + return -1; + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFilterProperties.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFilterProperties.java new file mode 100644 index 0000000000..4bf6453355 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFilterProperties.java @@ -0,0 +1,47 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("logging.global") +public class LoggingGlobalFilterProperties { + + private boolean enabled; + private boolean requestHeaders; + private boolean requestBody; + private boolean responseHeaders; + private boolean responseBody; + + public boolean isEnabled() { + return enabled; + } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean isRequestHeaders() { + return requestHeaders; + } + public void setRequestHeaders(boolean requestHeaders) { + this.requestHeaders = requestHeaders; + } + public boolean isRequestBody() { + return requestBody; + } + public void setRequestBody(boolean requestBody) { + this.requestBody = requestBody; + } + public boolean isResponseHeaders() { + return responseHeaders; + } + public void setResponseHeaders(boolean responseHeaders) { + this.responseHeaders = responseHeaders; + } + public boolean isResponseBody() { + return responseBody; + } + public void setResponseBody(boolean responseBody) { + this.responseBody = responseBody; + } + + + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFiltersConfigurations.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFiltersConfigurations.java new file mode 100644 index 0000000000..2dead8da3b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalFiltersConfigurations.java @@ -0,0 +1,25 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import reactor.core.publisher.Mono; + +@Configuration +public class LoggingGlobalFiltersConfigurations { + + final Logger logger = LoggerFactory.getLogger(LoggingGlobalFiltersConfigurations.class); + + @Bean + public GlobalFilter postGlobalFilter() { + return (exchange, chain) -> { + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + logger.info("Global Post Filter executed"); + })); + }; + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalPreFilter.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalPreFilter.java new file mode 100644 index 0000000000..2bacf033db --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/global/LoggingGlobalPreFilter.java @@ -0,0 +1,22 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +@Component +public class LoggingGlobalPreFilter implements GlobalFilter { + + final Logger logger = LoggerFactory.getLogger(LoggingGlobalPreFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + logger.info("Global Pre Filter executed"); + return chain.filter(exchange); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/routes/ServiceRouteConfiguration.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/routes/ServiceRouteConfiguration.java new file mode 100644 index 0000000000..17d4827d93 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/routes/ServiceRouteConfiguration.java @@ -0,0 +1,28 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.routes; + +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; + +import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.LoggingGatewayFilterFactory; +import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.LoggingGatewayFilterFactory.Config; + +/** + * Note: We want to keep this as an example of configuring a Route with a custom filter + * + * This corresponds with the properties configuration we have + */ +// @Configuration +public class ServiceRouteConfiguration { + + @Bean + public RouteLocator routes(RouteLocatorBuilder builder, LoggingGatewayFilterFactory loggingFactory) { + + return builder.routes() + .route("service_route_java_config", r -> r.path("/service/**") + .filters(f -> f.rewritePath("/service(?/?.*)", "$\\{segment}") + .filter(loggingFactory.apply(new Config("My Custom Message", true, true)))) + .uri("http://localhost:8081")) + .build(); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceApplication.java new file mode 100644 index 0000000000..b65d0efbd6 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.customfilters.secondservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:secondservice-application.properties") +public class SecondServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(SecondServiceApplication.class, args); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/web/SecondServiceRestController.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/web/SecondServiceRestController.java new file mode 100644 index 0000000000..1ac44ba6b1 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/secondservice/web/SecondServiceRestController.java @@ -0,0 +1,18 @@ +package com.baeldung.springcloudgateway.customfilters.secondservice.web; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Mono; + +@RestController +public class SecondServiceRestController { + + @GetMapping("/resource/language") + public Mono> getResource() { + return Mono.just(ResponseEntity.ok() + .body("es")); + + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/ServiceApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/ServiceApplication.java new file mode 100644 index 0000000000..1e2ffb63c2 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/ServiceApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.customfilters.service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:service-application.properties") +public class ServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ServiceApplication.class, args); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/web/ServiceRestController.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/web/ServiceRestController.java new file mode 100644 index 0000000000..3ca09f6853 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/customfilters/service/web/ServiceRestController.java @@ -0,0 +1,22 @@ +package com.baeldung.springcloudgateway.customfilters.service.web; + +import java.util.Locale; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Mono; + +@RestController +public class ServiceRestController { + + @GetMapping("/resource") + public Mono> getResource() { + return Mono.just(ResponseEntity.ok() + .header(HttpHeaders.CONTENT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .body("Service Resource")); + + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplication.java new file mode 100644 index 0000000000..e209b6cdf0 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.custompredicates; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class CustomPredicatesApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(CustomPredicatesApplication.class) + .profiles("customroutes") + .run(args); + } + +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/config/CustomPredicatesConfig.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/config/CustomPredicatesConfig.java new file mode 100644 index 0000000000..ea58eb7e46 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/config/CustomPredicatesConfig.java @@ -0,0 +1,43 @@ +package com.baeldung.springcloudgateway.custompredicates.config; + +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.baeldung.springcloudgateway.custompredicates.factories.GoldenCustomerRoutePredicateFactory; +import com.baeldung.springcloudgateway.custompredicates.factories.GoldenCustomerRoutePredicateFactory.Config; +import com.baeldung.springcloudgateway.custompredicates.service.GoldenCustomerService; + +@Configuration +public class CustomPredicatesConfig { + + + @Bean + public GoldenCustomerRoutePredicateFactory goldenCustomer(GoldenCustomerService goldenCustomerService) { + return new GoldenCustomerRoutePredicateFactory(goldenCustomerService); + } + + + //@Bean + public RouteLocator routes(RouteLocatorBuilder builder, GoldenCustomerRoutePredicateFactory gf ) { + + return builder.routes() + .route("dsl_golden_route", r -> + r.predicate(gf.apply(new Config(true, "customerId"))) + .and() + .path("/dsl_api/**") + .filters(f -> f.stripPrefix(1)) + .uri("https://httpbin.org") + ) + .route("dsl_common_route", r -> + r.predicate(gf.apply(new Config(false, "customerId"))) + .and() + .path("/dsl_api/**") + .filters(f -> f.stripPrefix(1)) + .uri("https://httpbin.org") + ) + .build(); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/factories/GoldenCustomerRoutePredicateFactory.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/factories/GoldenCustomerRoutePredicateFactory.java new file mode 100644 index 0000000000..cb5c3a0b50 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/factories/GoldenCustomerRoutePredicateFactory.java @@ -0,0 +1,102 @@ +/** + * + */ +package com.baeldung.springcloudgateway.custompredicates.factories; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import javax.validation.constraints.NotEmpty; + +import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; +import org.springframework.http.HttpCookie; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.server.ServerWebExchange; + +import com.baeldung.springcloudgateway.custompredicates.service.GoldenCustomerService; + +/** + * @author Philippe + * + */ +public class GoldenCustomerRoutePredicateFactory extends AbstractRoutePredicateFactory { + + private final GoldenCustomerService goldenCustomerService; + + public GoldenCustomerRoutePredicateFactory(GoldenCustomerService goldenCustomerService ) { + super(Config.class); + this.goldenCustomerService = goldenCustomerService; + } + + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("isGolden","customerIdCookie"); + } + + + @Override + public Predicate apply(Config config) { + + return (ServerWebExchange t) -> { + List cookies = t.getRequest() + .getCookies() + .get(config.getCustomerIdCookie()); + + boolean isGolden; + if ( cookies == null || cookies.isEmpty()) { + isGolden = false; + } + else { + String customerId = cookies.get(0).getValue(); + isGolden = goldenCustomerService.isGoldenCustomer(customerId); + } + + return config.isGolden()?isGolden:!isGolden; + }; + } + + + @Validated + public static class Config { + boolean isGolden = true; + + @NotEmpty + String customerIdCookie = "customerId"; + + + public Config() {} + + public Config( boolean isGolden, String customerIdCookie) { + this.isGolden = isGolden; + this.customerIdCookie = customerIdCookie; + } + + public boolean isGolden() { + return isGolden; + } + + public void setGolden(boolean value) { + this.isGolden = value; + } + + /** + * @return the customerIdCookie + */ + public String getCustomerIdCookie() { + return customerIdCookie; + } + + /** + * @param customerIdCookie the customerIdCookie to set + */ + public void setCustomerIdCookie(String customerIdCookie) { + this.customerIdCookie = customerIdCookie; + } + + + + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/service/GoldenCustomerService.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/service/GoldenCustomerService.java new file mode 100644 index 0000000000..82bf2e6ae9 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/custompredicates/service/GoldenCustomerService.java @@ -0,0 +1,26 @@ +/** + * + */ +package com.baeldung.springcloudgateway.custompredicates.service; + +import org.springframework.stereotype.Component; + +/** + * @author Philippe + * + */ +@Component +public class GoldenCustomerService { + + public boolean isGoldenCustomer(String customerId) { + + // TODO: Add some AI logic to check is this customer deserves a "golden" status ;^) + if ( "baeldung".equalsIgnoreCase(customerId)) { + return true; + } + else { + return false; + } + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java new file mode 100644 index 0000000000..d276597a6b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.introduction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:introduction-application.properties") +public class IntroductionGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(IntroductionGatewayApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/QuotesApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/QuotesApplication.java new file mode 100644 index 0000000000..96daf8a73d --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/QuotesApplication.java @@ -0,0 +1,44 @@ +/** + * + */ +package com.baeldung.springcloudgateway.oauth.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; + +import com.baeldung.springcloudgateway.oauth.shared.KeycloakReactiveTokenInstrospector; + +/** + * @author Philippe + * + */ +@SpringBootApplication +@PropertySource("classpath:quotes-application.properties") +@EnableWebFluxSecurity +public class QuotesApplication { + + public static void main(String[] args) { + SpringApplication.run(QuotesApplication.class); + } + + + @Bean + public ReactiveOpaqueTokenIntrospector keycloakIntrospector(OAuth2ResourceServerProperties props) { + + NimbusReactiveOpaqueTokenIntrospector delegate = new NimbusReactiveOpaqueTokenIntrospector( + props.getOpaquetoken().getIntrospectionUri(), + props.getOpaquetoken().getClientId(), + props.getOpaquetoken().getClientSecret()); + + return new KeycloakReactiveTokenInstrospector(delegate); + } + + +} + diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/domain/Quote.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/domain/Quote.java new file mode 100644 index 0000000000..042cfa63fa --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/domain/Quote.java @@ -0,0 +1,35 @@ +package com.baeldung.springcloudgateway.oauth.backend.domain; + + +public class Quote { + + private String symbol; + private double price; + + /** + * @return the symbol + */ + public String getSymbol() { + return symbol; + } + /** + * @param symbol the symbol to set + */ + public void setSymbol(String symbol) { + this.symbol = symbol; + } + /** + * @return the price + */ + public double getPrice() { + return price; + } + /** + * @param price the price to set + */ + public void setPrice(double price) { + this.price = price; + } + + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/web/QuoteApi.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/web/QuoteApi.java new file mode 100644 index 0000000000..6d3721166c --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/backend/web/QuoteApi.java @@ -0,0 +1,34 @@ +package com.baeldung.springcloudgateway.oauth.backend.web; + +import javax.annotation.PostConstruct; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.springcloudgateway.oauth.backend.domain.Quote; + +import reactor.core.publisher.Mono; + +@RestController +public class QuoteApi { + private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold"); + + @GetMapping("/quotes/{symbol}") + public Mono getQuote(@PathVariable("symbol") String symbol, BearerTokenAuthentication auth ) { + + Quote q = new Quote(); + q.setSymbol(symbol); + + if ( auth.getAuthorities().contains(GOLD_CUSTOMER)) { + q.setPrice(10.0); + } + else { + q.setPrice(12.0); + } + return Mono.just(q); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/server/ResourceServerGatewayApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/server/ResourceServerGatewayApplication.java new file mode 100644 index 0000000000..1b85867f3b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/server/ResourceServerGatewayApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.springcloudgateway.oauth.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; + +@SpringBootApplication +@EnableWebFluxSecurity +public class ResourceServerGatewayApplication { + public static void main(String[] args) { + SpringApplication.run(ResourceServerGatewayApplication.class,args); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/shared/KeycloakReactiveTokenInstrospector.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/shared/KeycloakReactiveTokenInstrospector.java new file mode 100644 index 0000000000..e834e6934d --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/oauth/shared/KeycloakReactiveTokenInstrospector.java @@ -0,0 +1,65 @@ +/** + * + */ +package com.baeldung.springcloudgateway.oauth.shared; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; + +import reactor.core.publisher.Mono; + +/** + * Custom ReactiveTokenIntrospector to map realm roles into Spring GrantedAuthorities + * + */ +public class KeycloakReactiveTokenInstrospector implements ReactiveOpaqueTokenIntrospector { + + private final ReactiveOpaqueTokenIntrospector delegate; + + public KeycloakReactiveTokenInstrospector(ReactiveOpaqueTokenIntrospector delegate) { + this.delegate = delegate; + } + + @Override + public Mono introspect(String token) { + + return delegate.introspect(token) + .map( this::mapPrincipal); + } + + protected OAuth2AuthenticatedPrincipal mapPrincipal(OAuth2AuthenticatedPrincipal principal) { + + return new DefaultOAuth2AuthenticatedPrincipal( + principal.getName(), + principal.getAttributes(), + extractAuthorities(principal)); + } + + protected Collection extractAuthorities(OAuth2AuthenticatedPrincipal principal) { + + // + Map> realm_access = principal.getAttribute("realm_access"); + List roles = realm_access.getOrDefault("roles", Collections.emptyList()); + List rolesAuthorities = roles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + Set allAuthorities = new HashSet<>(); + allAuthorities.addAll(principal.getAuthorities()); + allAuthorities.addAll(rolesAuthorities); + + return allAuthorities; + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplication.java new file mode 100644 index 0000000000..46541b4826 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplication.java @@ -0,0 +1,25 @@ +/** + * + */ +package com.baeldung.springcloudgateway.rewrite; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.gateway.filter.GatewayFilter; + +import reactor.core.publisher.Mono; + +/** + * @author Baeldung + * + */ +@SpringBootApplication +public class URLRewriteGatewayApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(URLRewriteGatewayApplication.class) + .profiles("url-rewrite") + .run(args); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/routes/DynamicRewriteRoute.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/routes/DynamicRewriteRoute.java new file mode 100644 index 0000000000..29d40d7021 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/rewrite/routes/DynamicRewriteRoute.java @@ -0,0 +1,43 @@ +package com.baeldung.springcloudgateway.rewrite.routes; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; + +import java.util.Random; + +@Configuration +@Profile("url-rewrite") +public class DynamicRewriteRoute { + + @Value("${rewrite.backend.uri}") + private String backendUri; + private static Random rnd = new Random(); + + @Bean + public RouteLocator dynamicZipCodeRoute(RouteLocatorBuilder builder) { + return builder.routes() + .route("dynamicRewrite", r -> + r.path("/v2/zip/**") + .filters(f -> f.filter((exchange, chain) -> { + ServerHttpRequest req = exchange.getRequest(); + addOriginalRequestUrl(exchange, req.getURI()); + String path = req.getURI().getRawPath(); + String newPath = path.replaceAll( + "/v2/zip/(?.*)", + "/api/zip/${zipcode}-" + String.format("%03d", rnd.nextInt(1000))); + ServerHttpRequest request = req.mutate().path(newPath).build(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI()); + return chain.filter(exchange.mutate().request(request).build()); + })) + .uri(backendUri)) + .build(); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java new file mode 100644 index 0000000000..9e212cc4bf --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.webfilters; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class WebFilterGatewayApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(WebFilterGatewayApplication.class) + .profiles("url-rewrite") + .run(args); + } + +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java new file mode 100644 index 0000000000..7b6188b66a --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java @@ -0,0 +1,49 @@ +package com.baeldung.springcloudgateway.webfilters.config; + +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; + +import reactor.core.publisher.Mono; + +@Configuration +public class ModifyBodyRouteConfig { + + @Bean + public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("modify_request_body", r -> r.path("/post") + .filters(f -> f.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, + (exchange, s) -> Mono.just(new Hello(s.toUpperCase())))).uri("https://httpbin.org")) + .build(); + } + + @Bean + public RouteLocator responseRoutes(RouteLocatorBuilder builder) { + return builder.routes() + .route("modify_response_body", r -> r.path("/put/**") + .filters(f -> f.modifyResponseBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, + (exchange, s) -> Mono.just(new Hello("New Body")))).uri("https://httpbin.org")) + .build(); + } + + static class Hello { + String message; + + public Hello() { } + + public Hello(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java new file mode 100644 index 0000000000..f80a742fa6 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.springcloudgateway.webfilters.config; + +import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import reactor.core.publisher.Mono; + +@Configuration +public class RequestRateLimiterResolverConfig { + + @Bean + KeyResolver userKeyResolver() { + return exchange -> Mono.just("1"); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-customroutes.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-customroutes.yml new file mode 100644 index 0000000000..859aa60bda --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-customroutes.yml @@ -0,0 +1,26 @@ +spring: + cloud: + gateway: + routes: + - id: golden_route + uri: https://httpbin.org + predicates: + - Path=/api/** + - GoldenCustomer=true + filters: + - StripPrefix=1 + - AddRequestHeader=GoldenCustomer,true + - id: common_route + uri: https://httpbin.org + predicates: + - Path=/api/** + - name: GoldenCustomer + args: + golden: false + customerIdCookie: customerId + filters: + - StripPrefix=1 + - AddRequestHeader=GoldenCustomer,false + + + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-nosecurity.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-nosecurity.yml new file mode 100644 index 0000000000..40a52ded0f --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-nosecurity.yml @@ -0,0 +1,8 @@ +# Enable this profile to disable security +spring: + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration + - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-oauth-client.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-oauth-client.yml new file mode 100644 index 0000000000..b097c54eb1 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-oauth-client.yml @@ -0,0 +1,26 @@ +server: + port: 8087 +spring: + cloud: + gateway: + redis: + enabled: false + routes: + - id: quotes + uri: http://localhost:8085 + predicates: + - Path=/quotes/** + filters: - TokenRelay= + security: + oauth2: + client: provider: keycloak: + issuer-uri: http://localhost:8083/auth/realms/baeldung + registration: quotes-client: + provider: keycloak + client-id: quotes-client + client-secret: 0e082231-a70d-48e8-b8a5-fbfb743041b6 + scope: - email + - profile + - roles + + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-resource-server.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-resource-server.yml new file mode 100644 index 0000000000..14f713a04a --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-resource-server.yml @@ -0,0 +1,19 @@ +server: + port: 8086 +spring: + security: + oauth2: + resourceserver: + opaquetoken: + introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect + client-id: quotes-client + client-secret: 0e082231-a70d-48e8-b8a5-fbfb743041b6 + cloud: + gateway: + redis: + enabled: false + routes: + - id: quotes + uri: http://localhost:8085 + predicates: + - Path=/quotes/** diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-scrub.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-scrub.yml new file mode 100644 index 0000000000..da7dfea0a7 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-scrub.yml @@ -0,0 +1,12 @@ +spring: + cloud: + gateway: + routes: + - id: rewrite_with_scrub + uri: ${rewrite.backend.uri:http://example.com} + predicates: + - Path=/v1/customer/** + filters: + - RewritePath=/v1/customer/(?.*),/api/$\{segment} + - ScrubResponse=ssn,*** + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-url-rewrite.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-url-rewrite.yml new file mode 100644 index 0000000000..b2656151db --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-url-rewrite.yml @@ -0,0 +1,11 @@ +spring: + cloud: + gateway: + routes: + - id: rewrite_v1 + uri: ${rewrite.backend.uri:http://example.com} + predicates: + - Path=/v1/customer/** + filters: + - RewritePath=/v1/customer/(?.*),/api/$\{segment} + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-webfilters.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-webfilters.yml new file mode 100644 index 0000000000..3348cbbba0 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application-webfilters.yml @@ -0,0 +1,102 @@ +logging: + level: + org.springframework.cloud.gateway: INFO + reactor.netty.http.client: INFO + +spring: + redis: + host: localhost + port: 6379 + cloud: + gateway: + routes: + - id: request_header_route + uri: https://httpbin.org + predicates: + - Path=/get/** + filters: + - AddRequestHeader=My-Header-Good,Good + - AddRequestHeader=My-Header-Remove,Remove + - AddRequestParameter=var, good + - AddRequestParameter=var2, remove + - MapRequestHeader=My-Header-Good, My-Header-Bad + - MapRequestHeader=My-Header-Set, My-Header-Bad + - SetRequestHeader=My-Header-Set, Set + - RemoveRequestHeader=My-Header-Remove + - RemoveRequestParameter=var2 + - PreserveHostHeader + + - id: response_header_route + uri: https://httpbin.org + predicates: + - Path=/header/post/** + filters: + - AddResponseHeader=My-Header-Good,Good + - AddResponseHeader=My-Header-Set,Good + - AddResponseHeader=My-Header-Rewrite, password=12345678 + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin + - AddResponseHeader=My-Header-Remove,Remove + - SetResponseHeader=My-Header-Set, Set + - RemoveResponseHeader=My-Header-Remove + - RewriteResponseHeader=My-Header-Rewrite, password=[^&]+, password=*** + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , + - StripPrefix=1 + + - id: path_route + uri: https://httpbin.org + predicates: + - Path=/new/post/** + filters: + - RewritePath=/new(?/?.*), $\{segment} + - SetPath=/post + + - id: redirect_route + uri: https://httpbin.org + predicates: + - Path=/fake/post/** + filters: + - RedirectTo=302, https://httpbin.org + + - id: status_route + uri: https://httpbin.org + predicates: + - Path=/delete/** + filters: + - SetStatus=401 + + - id: size_route + uri: https://httpbin.org + predicates: + - Path=/anything + filters: + - name: RequestSize + args: + maxSize: 5000000 + + - id: retry_test + uri: https://httpbin.org + predicates: + - Path=/status/502 + filters: + - name: Retry + args: + retries: 3 + statuses: BAD_GATEWAY + methods: GET,POST + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false + + - id: request_rate_limiter + uri: https://httpbin.org + predicates: + - Path=/redis/get/** + filters: + - StripPrefix=1 + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 5 + key-resolver: "#{@userKeyResolver}" \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application.yml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application.yml new file mode 100644 index 0000000000..a33bca2055 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/application.yml @@ -0,0 +1,4 @@ +logging: + level: + org.springframework.cloud.gateway: DEBUG + reactor.netty.http.client: DEBUG diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/customfilters-global-application.properties b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/customfilters-global-application.properties new file mode 100644 index 0000000000..08421a0653 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/customfilters-global-application.properties @@ -0,0 +1,19 @@ +spring.cloud.gateway.routes[0].id=service_route +spring.cloud.gateway.routes[0].uri=http://localhost:8081 +spring.cloud.gateway.routes[0].predicates[0]=Path=/service/** +spring.cloud.gateway.routes[0].filters[0]=RewritePath=/service(?/?.*), $\{segment} +spring.cloud.gateway.routes[0].filters[1]=Logging=My Custom Message, true, true +# Or, as an alternative: +#spring.cloud.gateway.routes[0].filters[1].name=Logging +#spring.cloud.gateway.routes[0].filters[1].args[baseMessage]=My Custom Message +#spring.cloud.gateway.routes[0].filters[1].args[preLogger]=true +#spring.cloud.gateway.routes[0].filters[1].args[postLogger]=true + +spring.cloud.gateway.routes[0].filters[2]=ModifyResponse +spring.cloud.gateway.routes[0].filters[3]=ModifyRequest=en +spring.cloud.gateway.routes[0].filters[4]=ChainRequest=http://localhost:8082/resource/language, fr + +management.endpoints.web.exposure.include=* + +server.port=80 + diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/introduction-application.properties b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/introduction-application.properties new file mode 100644 index 0000000000..d7a6c4e072 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/introduction-application.properties @@ -0,0 +1,7 @@ +spring.cloud.gateway.routes[0].id=baeldung_route +spring.cloud.gateway.routes[0].uri=http://www.baeldung.com +spring.cloud.gateway.routes[0].predicates[0]=Path=/baeldung + +management.endpoints.web.exposure.include=* + +server.port=80 diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/logback.xml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/logback.xml new file mode 100644 index 0000000000..7d900d8ea8 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/quotes-application.properties b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/quotes-application.properties new file mode 100644 index 0000000000..48e8999b1b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/quotes-application.properties @@ -0,0 +1,12 @@ +server.port=8085 +# Disable gateway & redis as we don't need them in this application +spring.cloud.gateway.enabled=false +spring.cloud.gateway.redis.enabled=false + +# Resource server settings +spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect +spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client +spring.security.oauth2.resourceserver.opaquetoken.client-secret=0e082231-a70d-48e8-b8a5-fbfb743041b6 + + + diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/secondservice-application.properties b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/secondservice-application.properties new file mode 100644 index 0000000000..3cf12afeb9 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/secondservice-application.properties @@ -0,0 +1 @@ +server.port=8082 diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/service-application.properties b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/service-application.properties new file mode 100644 index 0000000000..4d360de145 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/main/resources/service-application.properties @@ -0,0 +1 @@ +server.port=8081 diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersLiveTest.java new file mode 100644 index 0000000000..f49f8c68b6 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/CustomFiltersLiveTest.java @@ -0,0 +1,112 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import com.baeldung.springcloudgateway.customfilters.gatewayapp.utils.LoggerListAppender; + +import ch.qos.logback.classic.spi.ILoggingEvent; + +/** + * This test requires: + * * the service in com.baeldung.service running + * * the 'second service' in com.baeldung.secondservice running + * + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class CustomFiltersLiveTest { + + @LocalServerPort + String port; + + @Autowired + private WebTestClient client; + + @BeforeEach + public void clearLogList() { + LoggerListAppender.clearEventList(); + client = WebTestClient.bindToServer() + .baseUrl("http://localhost:" + port) + .build(); + } + + @Test + public void whenCallServiceThroughGateway_thenAllConfiguredFiltersGetExecuted() { + ResponseSpec response = client.get() + .uri("/service/resource") + .exchange(); + + response.expectStatus() + .isOk() + .expectHeader() + .doesNotExist("Bael-Custom-Language-Header") + .expectBody(String.class) + .isEqualTo("Service Resource"); + + assertThat(LoggerListAppender.getEvents()) + // Global Pre Filter + .haveAtLeastOne(eventContains("Global Pre Filter executed")) + // Global Post Filter + .haveAtLeastOne(eventContains("Global Post Filter executed")) + // Global Pre and Post Filter + .haveAtLeastOne(eventContains("First Pre Global Filter")) + .haveAtLeastOne(eventContains("Last Post Global Filter")) + // Logging Filter Factory + .haveAtLeastOne(eventContains("Pre GatewayFilter logging: My Custom Message")) + .haveAtLeastOne(eventContains("Post GatewayFilter logging: My Custom Message")) + // Modify Request + .haveAtLeastOne(eventContains("Modify request output - Request contains Accept-Language header:")) + .haveAtLeastOne(eventContainsExcept("Removed all query params: ", "locale")) + // Modify Response + .areNot(eventContains("Added custom header to Response")) + // Chain Request + .haveAtLeastOne(eventContains("Chain Request output - Request contains Accept-Language header:")); + } + + @Test + public void givenRequestWithLocaleQueryParam_whenCallServiceThroughGateway_thenAllConfiguredFiltersGetExecuted() { + ResponseSpec response = client.get() + .uri("/service/resource?locale=en") + .exchange(); + + response.expectStatus() + .isOk() + .expectHeader() + .exists("Bael-Custom-Language-Header") + .expectBody(String.class) + .isEqualTo("Service Resource"); + + assertThat(LoggerListAppender.getEvents()) + // Modify Response + .haveAtLeastOne(eventContains("Added custom header to Response")) + .haveAtLeastOne(eventContainsExcept("Removed all query params: ", "locale")); + } + + /** + * This condition will be successful if the event contains a substring + */ + private Condition eventContains(String substring) { + return new Condition(entry -> (substring == null || (entry.getFormattedMessage() != null && entry.getFormattedMessage() + .contains(substring))), String.format("entry with message '%s'", substring)); + } + + /** + * This condition will be successful if the event contains a substring, but not another one + */ + private Condition eventContainsExcept(String substring, String except) { + return new Condition(entry -> (substring == null || (entry.getFormattedMessage() != null && entry.getFormattedMessage() + .contains(substring) + && !entry.getFormattedMessage() + .contains(except))), + String.format("entry with message '%s'", substring)); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactoryUnitTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactoryUnitTest.java new file mode 100644 index 0000000000..667aabaddc --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterFactoryUnitTest.java @@ -0,0 +1,61 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Config; +import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Scrubber; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Mono; + +class ScrubResponseGatewayFilterFactoryUnitTest { + + private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n" + + " \"name\" : \"John Doe\",\r\n" + + " \"ssn\" : \"123-45-9999\",\r\n" + + " \"account\" : \"9999888877770000\"\r\n" + + "}"; + + + @Test + void givenJsonWithFieldsToScrub_whenApply_thenScrubFields() throws Exception{ + + JsonFactory jf = new JsonFactory(new ObjectMapper()); + JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB); + JsonNode root = parser.readValueAsTree(); + + Config config = new Config(); + config.setFields("ssn|account"); + config.setReplacement("*"); + Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config); + + JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block(); + assertNotNull(scrubbed); + assertEquals("*", scrubbed.get("ssn").asText()); + } + + @Test + void givenJsonWithoutFieldsToScrub_whenApply_theBodUnchanged() throws Exception{ + + JsonFactory jf = new JsonFactory(new ObjectMapper()); + JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB); + JsonNode root = parser.readValueAsTree(); + + Config config = new Config(); + config.setFields("xxxx"); + config.setReplacement("*"); + Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config); + + JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block(); + assertNotNull(scrubbed); + assertNotEquals("*", scrubbed.get("ssn").asText()); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterLiveTest.java new file mode 100644 index 0000000000..8906af774e --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/filters/factories/ScrubResponseGatewayFilterLiveTest.java @@ -0,0 +1,135 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collections; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.sun.net.httpserver.HttpServer; + +import reactor.netty.http.client.HttpClient; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class ScrubResponseGatewayFilterLiveTest { + + private static Logger log = LoggerFactory.getLogger(ScrubResponseGatewayFilterLiveTest.class); + + private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n" + + " \"name\" : \"John Doe\",\r\n" + + " \"ssn\" : \"123-45-9999\",\r\n" + + " \"account\" : \"9999888877770000\"\r\n" + + "}"; + + private static final String JSON_WITH_SCRUBBED_FIELDS = "{\r\n" + + " \"name\" : \"John Doe\",\r\n" + + " \"ssn\" : \"*\",\r\n" + + " \"account\" : \"9999888877770000\"\r\n" + + "}"; + + @LocalServerPort + String port; + + @Autowired + private WebTestClient client; + + @Autowired HttpServer server; + + @Test + public void givenRequestToScrubRoute_thenResponseScrubbed() { + + client.get() + .uri("/scrub") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectHeader() + .contentType(MediaType.APPLICATION_JSON) + .expectBody() + .json(JSON_WITH_SCRUBBED_FIELDS); + } + + + @TestConfiguration + public static class TestRoutesConfiguration { + + + @Bean + public RouteLocator scrubSsnRoute(RouteLocatorBuilder builder, ScrubResponseGatewayFilterFactory scrubFilterFactory, SetPathGatewayFilterFactory pathFilterFactory, HttpServer server ) { + + log.info("[I92] Creating scrubSsnRoute..."); + + int mockServerPort = server.getAddress().getPort(); + ScrubResponseGatewayFilterFactory.Config config = new ScrubResponseGatewayFilterFactory.Config(); + config.setFields("ssn"); + config.setReplacement("*"); + + SetPathGatewayFilterFactory.Config pathConfig = new SetPathGatewayFilterFactory.Config(); + pathConfig.setTemplate("/customer"); + + return builder.routes() + .route("scrub_ssn", + r -> r.path("/scrub") + .filters( + f -> f + .filter(scrubFilterFactory.apply(config)) + .filter(pathFilterFactory.apply(pathConfig))) + .uri("http://localhost:" + mockServerPort )) + .build(); + } + + @Bean + public SecurityWebFilterChain testFilterChain(ServerHttpSecurity http ) { + + // @formatter:off + return http.authorizeExchange() + .anyExchange() + .permitAll() + .and() + .build(); + // @formatter:on + } + + @Bean + public HttpServer mockServer() throws IOException { + + log.info("[I48] Starting mock server..."); + + HttpServer server = HttpServer.create(new InetSocketAddress(0),0); + server.createContext("/customer", (exchange) -> { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + + byte[] response = JSON_WITH_FIELDS_TO_SCRUB.getBytes("UTF-8"); + exchange.sendResponseHeaders(200,response.length); + exchange.getResponseBody().write(response); + }); + + server.setExecutor(null); + server.start(); + + log.info("[I65] Mock server started. port={}", server.getAddress().getPort()); + return server; + } + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/utils/LoggerListAppender.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/utils/LoggerListAppender.java new file mode 100644 index 0000000000..8e113b417b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/gatewayapp/utils/LoggerListAppender.java @@ -0,0 +1,25 @@ +package com.baeldung.springcloudgateway.customfilters.gatewayapp.utils; + +import java.util.ArrayList; +import java.util.List; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +public class LoggerListAppender extends AppenderBase { + + static private List events = new ArrayList<>(); + + @Override + protected void append(ILoggingEvent eventObject) { + events.add(eventObject); + } + + public static List getEvents() { + return events; + } + + public static void clearEventList() { + events.clear(); + } +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceIntegrationTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceIntegrationTest.java new file mode 100644 index 0000000000..f4b3d0f00d --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SecondServiceIntegrationTest.java @@ -0,0 +1,28 @@ +package com.baeldung.springcloudgateway.customfilters.secondservice; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.springcloudgateway.customfilters.secondservice.web.SecondServiceRestController; + +@WebFluxTest(controllers = SecondServiceRestController.class, + excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +public class SecondServiceIntegrationTest { + + @Autowired + private WebTestClient webClient; + + @Test + public void whenResourceLanguageEndpointCalled_thenRetrievesSpanishLanguageString() throws Exception { + this.webClient.get() + .uri("/resource/language") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("es"); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SpringContextTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SpringContextTest.java new file mode 100644 index 0000000000..eaf94c0a42 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/secondservice/SpringContextTest.java @@ -0,0 +1,12 @@ +package com.baeldung.springcloudgateway.customfilters.secondservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = SecondServiceApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/ServiceIntegrationTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/ServiceIntegrationTest.java new file mode 100644 index 0000000000..9990cd003c --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/ServiceIntegrationTest.java @@ -0,0 +1,31 @@ +package com.baeldung.springcloudgateway.customfilters.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.springcloudgateway.customfilters.service.web.ServiceRestController; + +@WebFluxTest(controllers = ServiceRestController.class, + excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +public class ServiceIntegrationTest { + + @Autowired + private WebTestClient webClient; + + @Test + public void whenResourceEndpointCalled_thenRetrievesResourceStringWithContentLanguageHeader() throws Exception { + this.webClient.get() + .uri("/resource") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(HttpHeaders.CONTENT_LANGUAGE, "en") + .expectBody(String.class) + .isEqualTo("Service Resource"); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/SpringContextTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/SpringContextTest.java new file mode 100644 index 0000000000..2a9b322d5e --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/customfilters/service/SpringContextTest.java @@ -0,0 +1,12 @@ +package com.baeldung.springcloudgateway.customfilters.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = ServiceApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplicationLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplicationLiveTest.java new file mode 100644 index 0000000000..d9988ceb5e --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/custompredicates/CustomPredicatesApplicationLiveTest.java @@ -0,0 +1,67 @@ +package com.baeldung.springcloudgateway.custompredicates; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.Before; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +/** + * This test requires + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("customroutes") +public class CustomPredicatesApplicationLiveTest { + + @LocalServerPort + String serverPort; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void givenNormalCustomer_whenCallHeadersApi_thenResponseForNormalCustomer() throws JSONException { + + String url = "http://localhost:" + serverPort + "/api/headers"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + JSONObject json = new JSONObject(response.getBody()); + JSONObject headers = json.getJSONObject("headers"); + assertThat(headers.getString("Goldencustomer")).isEqualTo("false"); + + } + + @Test + void givenGoldenCustomer_whenCallHeadersApi_thenResponseForGoldenCustomer() throws JSONException { + + String url = "http://localhost:" + serverPort + "/api/headers"; + RequestEntity request = RequestEntity + .get(URI.create(url)) + .header("Cookie", "customerId=baeldung") + .build(); + + ResponseEntity response = restTemplate.exchange(request, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + JSONObject json = new JSONObject(response.getBody()); + JSONObject headers = json.getJSONObject("headers"); + assertThat(headers.getString("Goldencustomer")).isEqualTo("true"); + + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java new file mode 100644 index 0000000000..1550265f22 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.introduction; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import com.baeldung.springcloudgateway.introduction.IntroductionGatewayApplication; + + +@SpringBootTest(classes = IntroductionGatewayApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplicationLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplicationLiveTest.java new file mode 100644 index 0000000000..41fe37045c --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/rewrite/URLRewriteGatewayApplicationLiveTest.java @@ -0,0 +1,109 @@ +package com.baeldung.springcloudgateway.rewrite; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.OutputStream; +import java.net.InetSocketAddress; + +import org.junit.AfterClass; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.sun.net.httpserver.HttpServer; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles({ "nosecurity", "url-rewrite" }) +class URLRewriteGatewayApplicationLiveTest { + + // NOTE for Eclipse users: By default, Eclipse will complain about com.sun.** classes. + // To solve this issue, follow instructions available at the : + // https://stackoverflow.com/questions/13155734/eclipse-cant-recognize-com-sun-net-httpserver-httpserver-package + private static HttpServer mockServer; + private static Logger log = LoggerFactory.getLogger(URLRewriteGatewayApplicationLiveTest.class); + + // Create a running HttpServer that echoes back the request URL. + private static HttpServer startTestServer() { + + try { + log.info("[I26] Starting mock server"); + mockServer = HttpServer.create(); + mockServer.bind(new InetSocketAddress(0), 0); + mockServer.createContext("/api", (xchg) -> { + String uri = xchg.getRequestURI() + .toString(); + log.info("[I23] Backend called: uri={}", uri); + xchg.getResponseHeaders() + .add("Content-Type", "text/plain"); + xchg.sendResponseHeaders(200, 0); + OutputStream os = xchg.getResponseBody(); + os.write(uri.getBytes()); + os.flush(); + os.close(); + }); + + mockServer.start(); + InetSocketAddress localAddr = mockServer.getAddress(); + log.info("[I36] mock server started: local address={}", localAddr); + + return mockServer; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + } + + // TIP: https://www.baeldung.com/spring-dynamicpropertysource + @DynamicPropertySource + static void registerBackendServer(DynamicPropertyRegistry registry) { + registry.add("rewrite.backend.uri", () -> { + HttpServer s = startTestServer(); + return "http://localhost:" + s.getAddress().getPort(); + }); + } + + @AfterClass + public static void stopMockBackend() throws Exception { + log.info("[I40] Shutdown mock http server"); + mockServer.stop(5); + } + + @LocalServerPort + private int localPort; + + @Test + void testWhenApiCall_thenRewriteSuccess(@Autowired WebTestClient webClient) { + webClient.get() + .uri("http://localhost:" + localPort + "/v1/customer/customer1") + .exchange() + .expectBody() + .consumeWith((result) -> { + String body = new String(result.getResponseBody()); + log.info("[I99] body={}", body); + assertEquals("/api/customer1", body); + }); + } + + @Test + void testWhenDslCall_thenRewriteSuccess(@Autowired WebTestClient webClient) { + webClient.get() + .uri("http://localhost:" + localPort + "/v2/zip/123456") + .exchange() + .expectBody() + .consumeWith((result) -> { + String body = new String(result.getResponseBody()); + log.info("[I99] body={}", body); + assertTrue(body.matches("/api/zip/123456-\\d{3}")); + }); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java new file mode 100644 index 0000000000..a28eb68775 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java @@ -0,0 +1,64 @@ +package com.baeldung.springcloudgateway.webfilters; + +import org.junit.After; +import org.junit.Before; +import org.junit.jupiter.api.RepeatedTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +import redis.embedded.RedisServer; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("webfilters") +@TestConfiguration +public class RedisWebFilterFactoriesLiveTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(RedisWebFilterFactoriesLiveTest.class); + + private RedisServer redisServer; + + public RedisWebFilterFactoriesLiveTest() { + } + + @Before + public void postConstruct() { + this.redisServer = new RedisServer(6379); + redisServer.start(); + } + + @LocalServerPort + String port; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + TestRestTemplate template; + + @RepeatedTest(25) + public void whenCallRedisGetThroughGateway_thenOKStatusOrIsReceived() { + String url = "http://localhost:" + port + "/redis/get"; + + ResponseEntity r = restTemplate.getForEntity(url, String.class); + // assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + LOGGER.info("Received: status->{}, reason->{}, remaining->{}", + r.getStatusCodeValue(), r.getStatusCode().getReasonPhrase(), + r.getHeaders().get("X-RateLimit-Remaining")); + } + + @After + public void preDestroy() { + redisServer.stop(); + } + +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java new file mode 100644 index 0000000000..67e00a42fc --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java @@ -0,0 +1,136 @@ +package com.baeldung.springcloudgateway.webfilters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.assertj.core.api.Condition; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("webfilters") +public class WebFilterFactoriesLiveTest { + + @LocalServerPort + String port; + + @Autowired + private WebTestClient client; + + @Autowired + private TestRestTemplate restTemplate; + + @BeforeEach + public void configureClient() { + client = WebTestClient.bindToServer() + .baseUrl("http://localhost:" + port) + .build(); + } + + @Test + public void whenCallGetThroughGateway_thenAllHTTPRequestHeadersParametersAreSet() throws JSONException { + String url = "http://localhost:" + port + "/get"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + JSONObject json = new JSONObject(response.getBody()); + JSONObject headers = json.getJSONObject("headers"); + assertThat(headers.getString("My-Header-Good")).isEqualTo("Good"); + assertThat(headers.getString("My-Header-Bad")).isEqualTo("Good"); + assertThat(headers.getString("My-Header-Set")).isEqualTo("Set"); + assertTrue(headers.isNull("My-Header-Remove")); + JSONObject vars = json.getJSONObject("args"); + assertThat(vars.getString("var")).isEqualTo("good"); + } + + @Test + public void whenCallHeaderPostThroughGateway_thenAllHTTPResponseHeadersAreSet() { + ResponseSpec response = client.post() + .uri("/header/post") + .exchange(); + + response.expectStatus() + .isOk() + .expectHeader() + .valueEquals("My-Header-Rewrite", "password=***") + .expectHeader() + .valueEquals("My-Header-Set", "Set") + .expectHeader() + .valueEquals("My-Header-Good", "Good") + .expectHeader() + .doesNotExist("My-Header-Remove"); + } + + @Test + public void whenCallPostThroughGateway_thenBodyIsRetrieved() throws JSONException { + String url = "http://localhost:" + port + "/post"; + + HttpEntity entity = new HttpEntity<>("content", new HttpHeaders()); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + JSONObject json = new JSONObject(response.getBody()); + JSONObject data = json.getJSONObject("json"); + assertThat(data.getString("message")).isEqualTo("CONTENT"); + } + + @Test + public void whenCallPutThroughGateway_thenBodyIsRetrieved() throws JSONException { + String url = "http://localhost:" + port + "/put"; + + HttpEntity entity = new HttpEntity<>("CONTENT", new HttpHeaders()); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + JSONObject json = new JSONObject(response.getBody()); + assertThat(json.getString("message")).isEqualTo("New Body"); + } + + @Test + public void whenCallDeleteThroughGateway_thenIsUnauthorizedCodeIsSet() { + ResponseSpec response = client.delete() + .uri("/delete") + .exchange(); + + response.expectStatus() + .isUnauthorized(); + } + + @Test + public void whenCallFakePostThroughGateway_thenIsUnauthorizedCodeIsSet() { + ResponseSpec response = client.post() + .uri("/fake/post") + .exchange(); + + response.expectStatus() + .is3xxRedirection(); + } + + @Test + public void whenCallStatus504ThroughGateway_thenCircuitBreakerIsExecuted() throws JSONException { + String url = "http://localhost:" + port + "/status/504"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + JSONObject json = new JSONObject(response.getBody()); + assertThat(json.getString("url")).contains("anything"); + } +} diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/postman/OAuth_Gateway.postman_collection.json b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/postman/OAuth_Gateway.postman_collection.json new file mode 100644 index 0000000000..ac920a271b --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/postman/OAuth_Gateway.postman_collection.json @@ -0,0 +1,203 @@ +{ + "info": { + "_postman_id": "b3d00e23-c2cd-40ce-a90b-673efb25e5c0", + "name": "Baeldung - OAuth", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();\r", + "pm.environment.set(\"access_token\", jsonData.access_token);\r", + "pm.environment.set(\"refresh_token\", jsonData.refresh_token);\r", + "pm.environment.set(\"backend_token\", \"Bearer \" + jsonData.access_token);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{client_id}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{client_secret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "password", + "type": "text" + }, + { + "key": "scope", + "value": "email roles profile", + "type": "text" + }, + { + "key": "username", + "value": "maxwell.smart", + "type": "text" + }, + { + "key": "password", + "value": "1234", + "type": "text" + } + ] + }, + "url": { + "raw": "{{keycloack_base}}/token", + "host": [ + "{{keycloack_base}}" + ], + "path": [ + "token" + ] + } + }, + "response": [] + }, + { + "name": "Quote", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "accept": true + } + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8085/quotes/:symbol", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8085", + "path": [ + "quotes", + ":symbol" + ], + "variable": [ + { + "key": "symbol", + "value": "IBM" + } + ] + } + }, + "response": [] + }, + { + "name": "Quote via Gateway", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "accept": true + } + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8086/quotes/:symbol", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8086", + "path": [ + "quotes", + ":symbol" + ], + "variable": [ + { + "key": "symbol", + "value": "IBM" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "keycloack_base", + "value": "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect" + }, + { + "key": "client_id", + "value": "quotes-client" + }, + { + "key": "client_secret", + "value": "56be94c8-b20a-4374-899c-e39cb022d3f8" + } + ] +} \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/resources/logback-test.xml b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..6980d119b1 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-bootstrap/gateway-2/src/test/resources/logback-test.xml @@ -0,0 +1,17 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-bootstrap/pom.xml b/spring-cloud-modules/spring-cloud-bootstrap/pom.xml index e7fe7e7485..f4d4073ef1 100644 --- a/spring-cloud-modules/spring-cloud-bootstrap/pom.xml +++ b/spring-cloud-modules/spring-cloud-bootstrap/pom.xml @@ -18,6 +18,7 @@ config discovery gateway + gateway-2 svc-book svc-rating customer-service