diff --git a/spring-session-docs/src/docs/asciidoc/index.adoc b/spring-session-docs/src/docs/asciidoc/index.adoc index 963ca9a7..2992374f 100644 --- a/spring-session-docs/src/docs/asciidoc/index.adoc +++ b/spring-session-docs/src/docs/asciidoc/index.adoc @@ -70,6 +70,10 @@ To get started with Spring Session, the best place to start is our Sample Applic | Demonstrates how to use Spring Session to replace the Spring WebFlux's `WebSession` with Redis. | +| {gh-samples-url}spring-session-sample-boot-webflux-custom-cookie[WebFlux with Custom Cookie] +| Demonstrates how to use Spring Session to customize the Session cookie in a WebFlux based application. +| + | {gh-samples-url}spring-session-sample-boot-redis-json[HttpSession with Redis JSON serialization] | Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using JSON serialization. | diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/spring-session-sample-boot-webflux-custom-cookie.gradle b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/spring-session-sample-boot-webflux-custom-cookie.gradle new file mode 100644 index 00000000..52eda6bb --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/spring-session-sample-boot-webflux-custom-cookie.gradle @@ -0,0 +1,17 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-session-data-redis') + compile "org.springframework.boot:spring-boot-starter-webflux" + compile "org.springframework.boot:spring-boot-starter-thymeleaf" + compile "org.springframework.boot:spring-boot-starter-data-redis" + compile "org.springframework.boot:spring-boot-devtools" + compile 'org.webjars:bootstrap' + + testCompile "org.springframework.boot:spring-boot-starter-test" + testCompile "org.junit.jupiter:junit-jupiter-api" + testRuntime "org.junit.jupiter:junit-jupiter-engine" + + integrationTestCompile seleniumDependencies + integrationTestCompile "org.testcontainers:testcontainers" +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/AttributeTests.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/AttributeTests.java new file mode 100644 index 00000000..1978df68 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/AttributeTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import org.testcontainers.containers.GenericContainer; +import sample.pages.HomePage; +import sample.pages.HomePage.Attribute; + +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.context.annotation.Bean; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eleftheria Stein + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class AttributeTests { + + private static final String DOCKER_IMAGE = "redis:5.0.9"; + + @LocalServerPort + private int port; + + private WebDriver driver; + + @BeforeEach + void setup() { + this.driver = new HtmlUnitDriver(); + } + + @AfterEach + void tearDown() { + this.driver.quit(); + } + + @Test + void home() { + HomePage home = HomePage.go(this.driver, this.port); + home.assertAt(); + } + + @Test + void noAttributes() { + HomePage home = HomePage.go(this.driver, this.port); + assertThat(home.attributes()).isEmpty(); + } + + @Test + void createAttribute() { + HomePage home = HomePage.go(this.driver, this.port); + // @formatter:off + home = home.form() + .attributeName("a") + .attributeValue("b") + .submit(HomePage.class); + // @formatter:on + + List attributes = home.attributes(); + assertThat(attributes).hasSize(1); + Attribute row = attributes.get(0); + assertThat(row.getAttributeName()).isEqualTo("a"); + assertThat(row.getAttributeValue()).isEqualTo("b"); + } + + @TestConfiguration + static class Config { + + @Bean + GenericContainer redisContainer() { + GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(6379); + redisContainer.start(); + return redisContainer; + } + + @Bean + LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(), + redisContainer().getFirstMappedPort()); + } + + } + +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/pages/HomePage.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/pages/HomePage.java new file mode 100644 index 00000000..3813cd05 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/java/sample/pages/HomePage.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.pages; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; +import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eleftheria Stein + */ +public class HomePage { + + private WebDriver driver; + + @FindBy(css = "form") + WebElement form; + + @FindBy(css = "table tbody tr") + List trs; + + List attributes; + + public HomePage(WebDriver driver) { + this.driver = driver; + this.attributes = new ArrayList<>(); + } + + private static void get(WebDriver driver, int port, String get) { + String baseUrl = "http://localhost:" + port; + driver.get(baseUrl + get); + } + + public static HomePage go(WebDriver driver, int port) { + get(driver, port, "/"); + return PageFactory.initElements(driver, HomePage.class); + } + + public void assertAt() { + assertThat(this.driver.getTitle()).isEqualTo("Session Attributes"); + } + + public List attributes() { + List rows = new ArrayList<>(); + for (WebElement tr : this.trs) { + rows.add(new Attribute(tr)); + } + this.attributes.addAll(rows); + return this.attributes; + } + + public Form form() { + return new Form(this.form); + } + + public class Form { + + @FindBy(name = "attributeName") + WebElement attributeName; + + @FindBy(name = "attributeValue") + WebElement attributeValue; + + @FindBy(css = "input[type=\"submit\"]") + WebElement submit; + + public Form(SearchContext context) { + PageFactory.initElements(new DefaultElementLocatorFactory(context), this); + } + + public Form attributeName(String text) { + this.attributeName.sendKeys(text); + return this; + } + + public Form attributeValue(String text) { + this.attributeValue.sendKeys(text); + return this; + } + + public T submit(Class page) { + this.submit.click(); + return PageFactory.initElements(HomePage.this.driver, page); + } + + } + + public static class Attribute { + + @FindBy(xpath = ".//td[1]") + WebElement attributeName; + + @FindBy(xpath = ".//td[2]") + WebElement attributeValue; + + public Attribute(SearchContext context) { + PageFactory.initElements(new DefaultElementLocatorFactory(context), this); + } + + /** + * @return the attributeName + */ + public String getAttributeName() { + return this.attributeName.getText(); + } + + /** + * @return the attributeValue + */ + public String getAttributeValue() { + return this.attributeValue.getText(); + } + + } + +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/resources/testcontainers.properties b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/resources/testcontainers.properties new file mode 100644 index 00000000..e3e83419 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/integration-test/resources/testcontainers.properties @@ -0,0 +1 @@ +ryuk.container.timeout=120 diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/CookieConfig.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/CookieConfig.java new file mode 100644 index 00000000..95a4f1a6 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/CookieConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.WebSessionIdResolver; + +/** + * @author Eleftheria Stein + */ +@Configuration +public class CookieConfig { + + @Bean + public WebSessionIdResolver webSessionIdResolver() { + CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver(); + resolver.setCookieName("JSESSIONID"); + resolver.addCookieInitializer((builder) -> builder.path("/")); + resolver.addCookieInitializer((builder) -> builder.sameSite("Strict")); + return resolver; + } + +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/HelloWebFluxApplication.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/HelloWebFluxApplication.java new file mode 100644 index 00000000..01faa494 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/HelloWebFluxApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Eleftheria Stein + */ +@SpringBootApplication +public class HelloWebFluxApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebFluxApplication.class, args); + } + +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionAttributeForm.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionAttributeForm.java new file mode 100644 index 00000000..b8945d91 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionAttributeForm.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +/** + * @author Eleftheria Stein + */ +public class SessionAttributeForm { + + private String attributeName; + + private String attributeValue; + + public String getAttributeName() { + return this.attributeName; + } + + public void setAttributeName(String attributeName) { + this.attributeName = attributeName; + } + + public String getAttributeValue() { + return this.attributeValue; + } + + public void setAttributeValue(String attributeValue) { + this.attributeValue = attributeValue; + } + +} diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionController.java b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionController.java new file mode 100644 index 00000000..c9ac4c35 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/SessionController.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.server.WebSession; + +// tag::class[] +@Controller +public class SessionController { + + @PostMapping("/session") + public String setAttribute(@ModelAttribute SessionAttributeForm sessionAttributeForm, WebSession session) { + session.getAttributes().put(sessionAttributeForm.getAttributeName(), sessionAttributeForm.getAttributeValue()); + return "redirect:/"; + } + + @GetMapping("/") + public String index(Model model, WebSession webSession) { + model.addAttribute("webSession", webSession); + return "index"; + } + +} +// tag::end[] diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/resources/application.properties b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/resources/templates/index.html b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/resources/templates/index.html new file mode 100644 index 00000000..de0ed762 --- /dev/null +++ b/spring-session-samples/spring-session-sample-boot-webflux-custom-cookie/src/main/resources/templates/index.html @@ -0,0 +1,42 @@ + + + + + + Session Attributes + + + +
+

Description

+

This application demonstrates how to customize the session cookie. Notice that the name of the cookie is JSESSIONID.

+ +

Try it

+ +
+ + + + + +
+ +
+ + + + + + + + + + + + + + +
Attribute NameAttribute Value
+
+ +