Compare commits

...

53 Commits

Author SHA1 Message Date
Rob Winch
8553b52431 Release 3.0.0-M3 2022-05-18 14:54:53 -05:00
Rob Winch
69d8fda4cc Update spring-data-bom:2022.0.0-M4 2022-05-18 14:41:48 -05:00
Rob Winch
a9f7a35ef3 Revert "Temporarily Disable Samples"
This reverts commit 2effbd19ab.
2022-05-18 14:41:02 -05:00
Rob Winch
679b6e57df Next Development Version 2022-05-18 13:09:22 -05:00
Rob Winch
1bc21d5187 Release 3.0.0-M2 2022-05-18 13:08:46 -05:00
Rob Winch
2effbd19ab Temporarily Disable Samples 2022-05-18 13:00:33 -05:00
Rob Winch
31275574ee Update to spring-security-bom:6.0.0-M5
Closes gh-2093
2022-05-18 12:04:25 -05:00
Rob Winch
e4201aea05 Update to spring-data-bom:2022.0.0-M3
Closes gh-9092
2022-05-18 12:04:25 -05:00
Rob Winch
3c134778d8 Update to spring-framework-bom:6.0.0-M4
Closes gh-2091
2022-05-18 12:04:25 -05:00
Rob Winch
bc51d842dc Update to jackson-bom:2.13.3
Clsoes gh-2090
2022-05-18 12:04:25 -05:00
Rob Winch
17610e7cc2 Update to reactor-bom:2020.0.19
Closes gh-2089
2022-05-18 12:04:15 -05:00
Rob Winch
3ce78f6cd0 Fix formatting 2022-05-13 17:49:29 -05:00
Rob Winch
21b0f60721 Add .sdkmanrc 2022-05-13 17:41:34 -05:00
Rob Winch
0dcdf5f147 Fix Thymeleaf Samples
Thymeleaf removed support for accessing the HttpServletRequest and HttpSession
automatically, so we need to add any properties we want to access as ModelAttributes

Closes gh-2076
2022-05-13 17:37:33 -05:00
Rob Winch
a0bf6a0e62 Restore all Samples 2022-05-13 17:36:52 -05:00
Rob Winch
d23b81f300 Fix spring-session-sample-boot-mongodb-traditional 2022-05-13 17:36:52 -05:00
Rob Winch
f33c5fe19a Fix spring-session-sample-boot-websocket 2022-05-13 17:36:52 -05:00
Rob Winch
e1c4b25671 Fix spring-session-sample-boot-hazelcast 2022-05-13 17:36:52 -05:00
Rob Winch
9bf18059d2 Fix xpring-session-sample-boot-jdbc 2022-05-13 17:36:52 -05:00
Rob Winch
342198cdfb Fix spring-session-sample-boot-redis-json 2022-05-13 17:36:52 -05:00
Rob Winch
c151a97227 Fix spring-session-sample-boot-findbyusername 2022-05-13 17:36:52 -05:00
Rob Winch
0cb6e0ebc9 Fix spring-session-sample-boot-redis-simple 2022-05-13 17:36:52 -05:00
Rob Winch
b4c3cefcf4 fix spring-session-sample-boot-redis 2022-05-13 17:36:52 -05:00
Rob Winch
2a6a9cfb78 Fix Formatting 2022-05-13 17:36:23 -05:00
Greg L. Turnquist
55f9bc9c37 Replace Flapdoodle with Testcontainers for MongoDB support.
For more details on this usage of Testcontainers, see https://bsideup.github.io/posts/local_development_with_testcontainers/

Related issues: https://github.com/spring-projects/spring-boot/issues/30863
2022-05-13 17:34:43 -05:00
Eleftheria Stein
c9cf1eab7b Update Htmlunit test dependency 2022-05-13 13:13:32 -05:00
Eleftheria Stein
003335df73 Update LocalServerPort import to new package 2022-05-13 13:12:38 -05:00
Eleftheria Stein
c3b8634fb4 Use Java 17 in antora pipeline 2022-04-29 11:38:12 +02:00
Eleftheria Stein
28e1ab1d8d Upgrade to Gradle 7.4.2 in buildSrc
Issue gh-2073
2022-04-29 10:56:02 +02:00
Eleftheria Stein
ff8750e9c1 Re-enable working samples
The Spring Boot servlet samples remain disalbed because of gh-2076.
2022-04-29 10:33:32 +02:00
Eleftheria Stein
e51dd2d1b0 Upgrade test dependencies 2022-04-29 10:28:34 +02:00
Eleftheria Stein
a6f24bc27e Upgrade MongoDB to 4.6.0
Closes gh-2075
2022-04-29 10:28:01 +02:00
Eleftheria Stein
42580c3a44 Upgrade samples to Spring Boot 3.0.0-SNAPSHOT #
Closes gh-2074
2022-04-29 10:25:48 +02:00
Eleftheria Stein
ea0aef9d97 Upgrade to Gradle 7.4.2
Closes gh-2073
2022-04-29 10:21:39 +02:00
Eleftheria Stein
0d458a4a5b Update Redis docs
Issue gh-1711
2022-04-29 09:03:57 +02:00
Eleftheria Stein
102027a456 Add Caffeine community extension
Closes gh-2039
2022-04-27 09:53:55 +02:00
Eleftheria Stein
e580a97c0c Fix link to Infinispan cache 2022-04-27 09:53:55 +02:00
Eleftheria Stein
7227949afb Fix formatting 2022-04-27 09:53:55 +02:00
Greg L. Turnquist
34d59a0ed9 Switch back to unicode for the DOT substitute character.
MongoDB doesn't support "." in field names, so a Private Use Area character was used. This was originally stored in unicode format, but delomboking the code caused it to get transformed into another encoding. This causes issues on certain systems when building the software, so we are converting it back to its unicode representation. The character has been the same throughout, ensuring binary compatilibity.

See: https://www.compart.com/en/unicode/U+F607

Related: d601e270fc (diff-57190a47726099e31fdf86b12b80206e2ae24feb28aacaf494b99557583df150L47)
Closes #2053.
2022-04-27 09:53:55 +02:00
Eleftheria Stein
8582b9706d Use simple Redis repository by default
Closes gh-1711
2022-04-14 13:17:28 +02:00
Jerome Prinet
14ecf21c94 Update Gradle Enterprise plugin to 3.9 2022-04-14 11:11:31 +02:00
John Blum
6fc4097c2e Switch to Spring Security BOM 6.0.0-SNAPSHOT 2022-04-11 15:41:24 -07:00
John Blum
a1cfbcae0c Switch to Spring Data BOM 2022.0.0-SNAPSHOT 2022-04-11 15:40:50 -07:00
John Blum
004cf6656b Switch to Spring Framework BOM 6.0.0-SNAPSHOT 2022-04-11 15:39:51 -07:00
Felix Scheinost
cde256e1a3 Fix bug in JDBC SaveMode.ON_GET_ATTRIBUTE
Closes gh-2040
2022-04-08 17:20:46 +02:00
Eleftheria Stein
63f7f7b0a9 Upgrade Spring Data to 2022.0.0-M3
Closes gh-2048
2022-04-01 18:21:23 +02:00
Eleftheria Stein
140cc75583 Make RedisSessionRepository.DEFAULT_KEY_NAMESPACE public
Closes gh-2043
2022-03-15 18:34:59 +01:00
Eleftheria Stein
e157700087 Update to Antora 3.0.1
Closes gh-2038
2022-03-11 15:48:26 +01:00
Eleftheria Stein
1b18d64220 Fix 2.6.2 reference docs
Closes gh-2035
2022-03-11 15:48:18 +01:00
Eleftheria Stein
14756984fd Document release process for 3.0.x
Issueh gh-2036
2022-02-24 13:02:33 +01:00
Eleftheria Stein
34199baded Update Websocket sample to be compatible with H2 2.0
Closes gh-2013
2022-01-27 14:12:23 +01:00
Ruslan Molchanov
cc5bb1f3a2 Fix memory leak with null principal in Redis 2022-01-20 09:52:44 +01:00
Eleftheria Stein
48cf6849fe Next development version 2022-01-18 12:30:47 +01:00
90 changed files with 1971 additions and 602 deletions

View File

@@ -14,6 +14,12 @@ jobs:
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Generate antora.yml
run: ./gradlew :spring-session-docs:generateAntora
- name: Extract Branch Name

6
.sdkmanrc Normal file
View File

@@ -0,0 +1,6 @@
# Use sdkman to run "sdk env" to initialize with correct JDK version
# Enable auto-env through the sdkman_auto_env config
# See https://sdkman.io/usage#config
# A summary is to add the following to ~/.sdkman/etc/config
# sdkman_auto_env=true
java=17.0.2-tem

110
RELEASE.adoc Normal file
View File

@@ -0,0 +1,110 @@
== 1. Update Dependencies
Dependencies are declared in `gradle/dependency-management.gradle`.
Update Spring Framework, Spring Security and Spring Data at a minimum.
Run all the checks:
[source,bash]
----
$ ./gradlew check
----
Create separate issues for each dependency update, aside from test dependencies which can be combined into a single commit.
== 2. Check All Issues are Closed
You can manually check at https://github.com/spring-projects/spring-session/milestones
== 3. Update Release Version
Update the version number in `gradle.properties` for the release, for example `3.0.0-M1`, `3.0.0-RC1`, `3.0.4`
== 4. Update Antora Version
You will need to update the antora.yml version.
For milestone / release candidate releases you should follow this format:
----
version: '3.0.0-RC1'
prerelease: 'true'
display_version: '3.0.0-RC1'
----
== 5. Build Locally
Run the build using
[source,bash]
----
$ ./gradlew check
----
== 6. Push the Release Commit
Push the commit and GitHub actions will build and deploy the artifacts.
Wait for the artifact to appear in https://repo1.maven.org/maven2/org/springframework/session/spring-session-core/
== 7. Tag the release
Tag the release and then push the tag
....
git tag 3.0.0-RC1
git push origin 3.0.0-RC1
....
== 8. Update to Next Development Version
Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml and then push
== 9. Update version on project pages
Update the versions on https://spring.io/projects for Spring Session Core, Spring Session Data Redis, Spring Session JDBC, Spring Session Hazelcast, and Spring Session MongoDB.
== 10. Update Release Notes on GitHub
Download
https://github.com/spring-io/github-changelog-generator/releases/latest[the
GitHub release notes generator]
* Generate the release notes
....
java -jar github-changelog-generator.jar \
--changelog.repository=spring-projects/spring-session \
$MILESTONE release-notes
....
Note 1: `+$MILESTONE+` is something like `+3.0.4+` or `+3.0.0-M1+`. +
Note 2: This will create a file on your filesystem
called `+release-notes+`.
* Copy the release notes to your clipboard (your mileage may vary with
the following command)
....
cat release-notes | xclip -selection clipboard
....
* Create the
https://github.com/spring-projects/spring-session/releases[release on
GitHub], associate it with the tag, and paste the generated notes.
== 11. Close / Create Milestone
* In
https://github.com/spring-projects/spring-session/milestones[GitHub
Milestones], create a new milestone for the next release version.
* Move any open issues from the existing milestone you just released to
the new milestone.
* Close the milestone for the release.
Note: Spring Session typically releases only one milestone (M1) and one release candidate (RC1).
== 12. Announce the release
* Announce via Slack on https://pivotal.slack.com/messages/spring-session[#spring-session], and tag any downstream Spring Session projects (e.g Spring Session for Apache Geode).
Note: Do not post on #spring-release or create a blog post. Those steps happen after the Spring Session BOM is released.

View File

@@ -4,7 +4,7 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.5.5'
springBootVersion = '3.0.0-SNAPSHOT'
}
repositories {

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
version=3.0.0-M1
version=3.0.0-M3

View File

@@ -1,11 +1,11 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:2020.0.15'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.1'
mavenBom 'io.projectreactor:reactor-bom:2020.0.19'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.3'
mavenBom 'org.junit:junit-bom:5.8.2'
mavenBom 'org.springframework:spring-framework-bom:6.0.0-M2'
mavenBom 'org.springframework.data:spring-data-bom:2022.0.0-M1'
mavenBom 'org.springframework.security:spring-security-bom:6.0.0-M1'
mavenBom 'org.springframework:spring-framework-bom:6.0.0-M4'
mavenBom 'org.springframework.data:spring-data-bom:2022.0.0-M4'
mavenBom 'org.springframework.security:spring-security-bom:6.0.0-M5'
mavenBom 'org.testcontainers:testcontainers-bom:1.16.2'
}
@@ -14,7 +14,7 @@ dependencyManagement {
dependency 'org.aspectj:aspectjweaver:1.9.7'
dependency 'ch.qos.logback:logback-core:1.2.10'
dependency 'com.google.code.findbugs:jsr305:3.0.2'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.h2database:h2:2.1.212'
dependency 'com.ibm.db2:jcc:11.5.6.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:21.4.0.0.1'
@@ -34,7 +34,7 @@ dependencyManagement {
entry 'mockito-junit-jupiter'
}
dependencySet(group: 'org.mongodb', version: '4.4.1') {
dependencySet(group: 'org.mongodb', version: '4.6.0') {
entry 'mongodb-driver-core'
entry 'mongodb-driver-sync'
entry 'mongodb-driver-reactivestreams'

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -6,7 +6,7 @@ pluginManagement {
}
plugins {
id "com.gradle.enterprise" version "3.8.1"
id "com.gradle.enterprise" version "3.9"
id "io.spring.ge.conventions" version "0.0.7"
}
@@ -19,23 +19,10 @@ include 'spring-session-docs'
include 'spring-session-hazelcast'
include 'spring-session-jdbc'
include 'spring-session-sample-javaconfig-custom-cookie'
project(':spring-session-sample-javaconfig-custom-cookie').projectDir = file('spring-session-samples/spring-session-sample-javaconfig-custom-cookie')
include 'spring-session-sample-javaconfig-jdbc'
project(':spring-session-sample-javaconfig-jdbc').projectDir = file('spring-session-samples/spring-session-sample-javaconfig-jdbc')
include 'spring-session-sample-javaconfig-redis'
project(':spring-session-sample-javaconfig-redis').projectDir = file('spring-session-samples/spring-session-sample-javaconfig-redis')
include 'spring-session-sample-misc-hazelcast'
project(':spring-session-sample-misc-hazelcast').projectDir = file('spring-session-samples/spring-session-sample-misc-hazelcast')
include 'spring-session-sample-xml-redis'
project(':spring-session-sample-xml-redis').projectDir = file('spring-session-samples/spring-session-sample-xml-redis')
include 'spring-session-sample-xml-jdbc'
project(':spring-session-sample-xml-jdbc').projectDir = file('spring-session-samples/spring-session-sample-xml-jdbc')
//file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
// include dir.name
// project(":$dir.name").projectDir = dir
//}
file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
include dir.name
project(":$dir.name").projectDir = dir
}
rootProject.children.each { project ->
project.buildFileName = "${project.name}.gradle"

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2022 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.
@@ -39,10 +39,17 @@ import org.springframework.session.Session;
public class MongoSession implements Session {
/**
* Mongo doesn't support {@literal dot} in field names. We replace it with a very
* rarely used character
* Mongo doesn't support {@literal dot} in field names. We replace it with a unicode
* character from the Private Use Area.
* <p>
* NOTE: This was originally stored in unicode format. Delomboking the code caused it
* to get converted to another encoding, which isn't supported on all systems, so we
* migrated back to unicode. The same character is being represented ensuring binary
* compatibility.
*
* See https://www.compart.com/en/unicode/U+F607
*/
private static final char DOT_COVER_CHAR = '';
private static final char DOT_COVER_CHAR = '\uF607';
private String id;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 the original author or authors.
* Copyright 2014-2022 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.
@@ -527,6 +527,30 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
assertThat(findByPrincipalName.keySet()).containsOnly(changeSessionId);
}
@Test // gh-1987
void changeSessionIdWhenPrincipalNameChangesFromNullThenIndexShouldNotBeCreated() {
String principalName = null;
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
RedisSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
RedisSession findById = this.repository.findById(toSave.getId());
String changeSessionId = findById.changeSessionId();
findById.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(findById);
Map<String, RedisSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(changeSessionId);
}
@Test
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
@@ -667,7 +691,7 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests", enableIndexingAndEvents = true)
static class Config extends BaseConfig {
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -26,13 +26,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -223,17 +220,9 @@ class RedisSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableSpringHttpSession
@EnableRedisHttpSession
static class Config extends BaseConfig {
@Bean
RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new RedisSessionRepository(redisTemplate);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -113,7 +113,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
}
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1, enableIndexingAndEvents = true)
static class Config extends BaseConfig {
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -101,7 +101,7 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests")
@EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests", enableIndexingAndEvents = true)
static class Config extends BaseConfig {
@Bean

View File

@@ -858,11 +858,13 @@ public class RedisIndexedSessionRepository
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(this.originalSessionId);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.add(sessionId);
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(this.originalSessionId);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.add(sessionId);
}
}
this.originalSessionId = sessionId;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2022 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.
@@ -42,7 +42,10 @@ import org.springframework.util.Assert;
*/
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
private static final String DEFAULT_KEY_NAMESPACE = "spring:session";
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_KEY_NAMESPACE = "spring:session";
private final RedisOperations<String, Object> sessionRedisOperations;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -33,6 +33,7 @@ import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -54,7 +55,8 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
* }
* </pre>
*
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead.
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} or
* {@link RedisIndexedHttpSessionConfiguration} instead.
*
* @author Rob Winch
* @author Vedran Pavic
@@ -64,7 +66,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Import(RedisHttpSessionConfigurationSelector.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
@@ -113,13 +115,6 @@ public @interface EnableRedisHttpSession {
*/
FlushMode flushMode() default FlushMode.ON_SAVE;
/**
* The cron expression for expired session cleanup job. By default runs every minute.
* @return the session cleanup cron expression
* @since 2.0.0
*/
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
/**
* Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
* only saves changes made to session.
@@ -128,4 +123,13 @@ public @interface EnableRedisHttpSession {
*/
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
/**
* Indicate whether the {@link SessionRepository} should publish session events and
* support fetching sessions by index. If true, a
* {@link RedisIndexedSessionRepository} will be used in place of
* {@link RedisSessionRepository}. This will result in slower performance.
* @return true if indexing and events should be enabled, false otherwise
*/
boolean enableIndexingAndEvents() default false;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -16,55 +16,35 @@
package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.Collections;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -83,86 +63,39 @@ import org.springframework.util.StringValueResolver;
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
private Executor redisTaskExecutor;
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private List<SessionRepositoryCustomizer<RedisSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
public RedisSessionRepository sessionRepository() {
RedisTemplate<String, Object> redisTemplate = createRedisTemplate();
RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
@@ -186,20 +119,6 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
* @param configureRedisAction the configureRedis to set. The default is
* {@link ConfigureNotifyKeyspaceEventsAction}.
*/
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
@@ -217,31 +136,9 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
this.redisTaskExecutor = redisTaskExecutor;
}
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
ObjectProvider<SessionRepositoryCustomizer<RedisSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@@ -273,14 +170,10 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
private RedisTemplate<String, Object> createRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
@@ -292,77 +185,4 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.
* Without the SessionDestroyedEvent resources may not get cleaned up properly. For
* example, the mapping of the Session to WebSocket connections may not get cleaned
* up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private ConfigureRedisAction configure;
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory,
ConfigureRedisAction configure) {
this.connectionFactory = connectionFactory;
this.configure = configure;
}
@Override
public void afterPropertiesSet() {
if (this.configure == ConfigureRedisAction.NO_OP) {
return;
}
RedisConnection connection = this.connectionFactory.getConnection();
try {
this.configure.configure(connection);
}
finally {
try {
connection.close();
}
catch (Exception ex) {
LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex);
}
}
}
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final RedisIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2022 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 org.springframework.session.data.redis.config.annotation.web.http;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
* Dynamically determines which session repository configuration to include using the
* {@link EnableRedisHttpSession} annotation.
*
* @author Eleftheria Stein
* @since 3.0
*/
final class RedisHttpSessionConfigurationSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importMetadata) {
if (!importMetadata.hasAnnotation(EnableRedisHttpSession.class.getName())) {
return new String[0];
}
EnableRedisHttpSession annotation = importMetadata.getAnnotations().get(EnableRedisHttpSession.class)
.synthesize();
if (annotation.enableIndexingAndEvents()) {
return new String[] { RedisIndexedHttpSessionConfiguration.class.getName() };
}
else {
return new String[] { RedisHttpSessionConfiguration.class.getName() };
}
}
}

View File

@@ -0,0 +1,361 @@
/*
* Copyright 2014-2022 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 org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Exposes the {@link SessionRepositoryFilter} as a bean named
* {@code springSessionRepositoryFilter}. In order to use this a single
* {@link RedisConnectionFactory} must be exposed as a Bean.
*
* @author Eleftheria Stein
* @since 3.0
*/
@Configuration(proxyBeanMethods = false)
public class RedisIndexedHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
private Executor redisTaskExecutor;
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setRedisNamespace(String namespace) {
this.redisNamespace = namespace;
}
@Deprecated
public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
setFlushMode(redisFlushMode.getFlushMode());
}
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
* @param configureRedisAction the configureRedis to set. The default is
* {@link ConfigureNotifyKeyspaceEventsAction}.
*/
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable();
if (redisConnectionFactoryToUse == null) {
redisConnectionFactoryToUse = redisConnectionFactory.getObject();
}
this.redisConnectionFactory = redisConnectionFactoryToUse;
}
@Autowired(required = false)
@Qualifier("springSessionDefaultRedisSerializer")
public void setDefaultRedisSerializer(RedisSerializer<Object> defaultRedisSerializer) {
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
this.redisTaskExecutor = redisTaskExecutor;
}
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> attributeMap = importMetadata
.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
String redisNamespaceValue = attributes.getString("redisNamespace");
if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
}
FlushMode flushMode = attributes.getEnum("flushMode");
RedisFlushMode redisFlushMode = attributes.getEnum("redisFlushMode");
if (flushMode == FlushMode.ON_SAVE && redisFlushMode != RedisFlushMode.ON_SAVE) {
flushMode = redisFlushMode.getFlushMode();
}
this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
redisTemplate.setDefaultSerializer(this.defaultRedisSerializer);
}
redisTemplate.setConnectionFactory(this.redisConnectionFactory);
redisTemplate.setBeanClassLoader(this.classLoader);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.
* Without the SessionDestroyedEvent resources may not get cleaned up properly. For
* example, the mapping of the Session to WebSocket connections may not get cleaned
* up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private ConfigureRedisAction configure;
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory,
ConfigureRedisAction configure) {
this.connectionFactory = connectionFactory;
this.configure = configure;
}
@Override
public void afterPropertiesSet() {
if (this.configure == ConfigureRedisAction.NO_OP) {
return;
}
RedisConnection connection = this.connectionFactory.getConnection();
try {
this.configure.configure(connection);
}
finally {
try {
connection.close();
}
catch (Exception ex) {
LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex);
}
}
}
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final RedisIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisIndexedHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -49,14 +49,14 @@ class EnableRedisKeyspaceNotificationsInitializerTests {
@Captor
ArgumentCaptor<String> options;
private RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
private RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
given(this.connectionFactory.getConnection()).willReturn(this.connection);
this.initializer = new RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.initializer = new RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.connectionFactory, new ConfigureNotifyKeyspaceEventsAction());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -23,11 +23,14 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -50,6 +53,15 @@ public class RedisHttpSessionConfigurationClassPathXmlApplicationContextTests {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -21,12 +21,17 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -52,7 +57,20 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
@Bean
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
RedisConnectionFactory redisConnectionFactory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(redisConnectionFactory.getConnection()).willReturn(connection);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return redisConnectionFactory;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -26,6 +26,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
@@ -34,8 +35,10 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -76,6 +79,15 @@ class RedisHttpSessionConfigurationOverrideDefaultSerializerTests {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -16,7 +16,7 @@
package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Map;
import java.time.Duration;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
@@ -32,23 +32,22 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -60,9 +59,7 @@ import static org.mockito.Mockito.mock;
*/
class RedisHttpSessionConfigurationTests {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private static final Duration MAX_INACTIVE_INTERVAL_DURATION = Duration.ofSeconds(600);
private AnnotationConfigApplicationContext context;
@@ -98,7 +95,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -106,7 +103,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -114,7 +111,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -122,40 +119,22 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customCleanupCronAnnotation() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class);
RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customCleanupCronSetter() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class);
RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@@ -163,7 +142,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -179,7 +158,7 @@ class RedisHttpSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -195,7 +174,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -211,7 +190,7 @@ class RedisHttpSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -230,32 +209,12 @@ class RedisHttpSessionConfigurationTests {
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test // gh-1252
void customRedisMessageListenerContainerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class);
Map<String, RedisMessageListenerContainer> beans = this.context
.getBeansOfType(RedisMessageListenerContainer.class);
assertThat(beans).hasSize(2);
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
MAX_INACTIVE_INTERVAL_DURATION);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
@@ -264,11 +223,24 @@ class RedisHttpSessionConfigurationTests {
}
private static RedisConnectionFactory mockRedisConnectionFactory() {
RedisConnectionFactory connectionFactory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(connectionFactory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
return connectionFactory;
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
Properties keyspaceEventsConfig = new Properties();
keyspaceEventsConfig.put("notify-keyspace-events", "KEA");
given(connectionMock.getConfig("notify-keyspace-events")).willReturn(keyspaceEventsConfig);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connectionMock).pSubscribe(any(), any());
return connectionFactoryMock;
}
@Configuration
@@ -323,20 +295,6 @@ class RedisHttpSessionConfigurationTests {
}
@EnableRedisHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION)
static class CustomCleanupCronExpressionAnnotationConfiguration {
}
@Configuration
static class CustomCleanupCronExpressionSetterConfiguration extends RedisHttpSessionConfiguration {
CustomCleanupCronExpressionSetterConfiguration() {
setCleanupCron(CLEANUP_CRON_EXPRESSION);
}
}
@EnableRedisHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
@@ -427,43 +385,20 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@EnableRedisHttpSession
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession
static class CustomRedisMessageListenerContainerConfig {
@Bean
RedisMessageListenerContainer redisMessageListenerContainer() {
return new RedisMessageListenerContainer();
}
}
@EnableRedisHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
SessionRepositoryCustomizer<RedisSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
SessionRepositoryCustomizer<RedisSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_DURATION);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -23,12 +23,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
@ExtendWith(SpringExtension.class)
@@ -46,6 +49,15 @@ public class RedisHttpSessionConfigurationXmlCustomExpireTests {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -23,12 +23,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
@ExtendWith(SpringExtension.class)
@@ -46,6 +49,15 @@ public class RedisHttpSessionConfigurationXmlTests {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -24,7 +24,7 @@ import org.mockito.MockitoAnnotations;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
import static org.mockito.BDDMockito.given;
@@ -33,7 +33,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class RedisHttpSessionConfigurationMockTests {
class RedisIndexedHttpSessionConfigurationMockTests {
@Mock
RedisConnectionFactory factory;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -27,8 +27,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -36,6 +36,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -47,7 +48,7 @@ import static org.mockito.Mockito.verify;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor {
@Autowired
RedisMessageListenerContainer redisMessageListenerContainer;
@@ -57,16 +58,22 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
@Test
void overrideSessionTaskExecutor() {
verify(this.springSessionRedisTaskExecutor, times(1)).execute(any(SchedulingAwareRunnable.class));
verify(this.springSessionRedisTaskExecutor, times(1)).execute(any(Runnable.class));
}
@EnableRedisHttpSession
@EnableRedisHttpSession(enableIndexingAndEvents = true)
@Configuration
static class Config {
@Bean
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
Executor executor = mock(Executor.class);
willAnswer((it) -> {
Runnable r = it.getArgument(0);
new Thread(r).start();
return null;
}).given(executor).execute(any());
return executor;
}
@Bean
@@ -76,6 +83,15 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -27,8 +27,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -36,6 +36,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -49,7 +50,7 @@ import static org.mockito.Mockito.verify;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors {
@Autowired
RedisMessageListenerContainer redisMessageListenerContainer;
@@ -62,22 +63,34 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
@Test
void overrideSessionTaskExecutors() {
verify(this.springSessionRedisSubscriptionExecutor, times(1)).execute(any(SchedulingAwareRunnable.class));
verify(this.springSessionRedisSubscriptionExecutor, times(1)).execute(any(Runnable.class));
verify(this.springSessionRedisTaskExecutor, never()).execute(any(Runnable.class));
}
@EnableRedisHttpSession
@EnableRedisHttpSession(enableIndexingAndEvents = true)
@Configuration
static class Config {
@Bean
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
Executor executor = mock(Executor.class);
willAnswer((it) -> {
Runnable r = it.getArgument(0);
new Thread(r).start();
return null;
}).given(executor).execute(any());
return executor;
}
@Bean
Executor springSessionRedisSubscriptionExecutor() {
return mock(Executor.class);
Executor executor = mock(Executor.class);
willAnswer((it) -> {
Runnable r = it.getArgument(0);
new Thread(r).start();
return null;
}).given(executor).execute(any());
return executor;
}
@Bean
@@ -87,6 +100,15 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -0,0 +1,471 @@
/*
* Copyright 2014-2022 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 org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisIndexedHttpSessionConfiguration}.
*/
class RedisIndexedHttpSessionConfigurationTests {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private AnnotationConfigApplicationContext context;
@BeforeEach
void before() {
this.context = new AnnotationConfigApplicationContext();
}
@AfterEach
void after() {
if (this.context != null) {
this.context.close();
}
}
@Test
void resolveValue() {
registerAndRefresh(RedisConfig.class, CustomRedisHttpSessionConfiguration.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("myRedisNamespace");
}
@Test
void resolveValueByPlaceholder() {
this.context
.setEnvironment(new MockEnvironment().withProperty("session.redis.namespace", "customRedisNamespace"));
registerAndRefresh(RedisConfig.class, PropertySourceConfiguration.class,
CustomRedisHttpSessionConfiguration2.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("customRedisNamespace");
}
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customCleanupCronSetter() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void multipleConnectionFactoryRedisConfig() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class))
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test // gh-1252
void customRedisMessageListenerContainerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class);
Map<String, RedisMessageListenerContainer> beans = this.context
.getBeansOfType(RedisMessageListenerContainer.class);
assertThat(beans).hasSize(2);
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
private static RedisConnectionFactory mockRedisConnectionFactory() {
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
Properties keyspaceEventsConfig = new Properties();
keyspaceEventsConfig.put("notify-keyspace-events", "KEA");
given(connectionMock.getConfig("notify-keyspace-events")).willReturn(keyspaceEventsConfig);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connectionMock).pSubscribe(any(), any());
return connectionFactoryMock;
}
@Configuration
static class PropertySourceConfiguration {
@Bean
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@Configuration
static class RedisConfig {
@Bean
RedisConnectionFactory defaultRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
static class CustomFlushImmediatelySetConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomFlushImmediatelySetConfiguration() {
setFlushMode(FlushMode.IMMEDIATE);
}
}
@Configuration
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomFlushImmediatelySetLegacyConfiguration() {
setRedisFlushMode(RedisFlushMode.IMMEDIATE);
}
}
@Configuration
@EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE, enableIndexingAndEvents = true)
static class CustomFlushImmediatelyConfiguration {
}
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE, enableIndexingAndEvents = true)
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelyLegacyConfiguration {
}
@Configuration
static class CustomCleanupCronExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomCleanupCronExpressionSetterConfiguration() {
setCleanupCron(CLEANUP_CRON_EXPRESSION);
}
}
@EnableRedisHttpSession(saveMode = SaveMode.ALWAYS, enableIndexingAndEvents = true)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class QualifiedConnectionFactoryRedisConfig {
@Bean
@SpringSessionRedisConnectionFactory
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class PrimaryConnectionFactoryRedisConfig {
@Bean
@Primary
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class QualifiedAndPrimaryConnectionFactoryRedisConfig {
@Bean
@SpringSessionRedisConnectionFactory
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@Bean
@Primary
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class NamedConnectionFactoryRedisConfig {
@Bean
RedisConnectionFactory redisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class MultipleConnectionFactoryRedisConfig {
@Bean
RedisConnectionFactory secondaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "myRedisNamespace", enableIndexingAndEvents = true)
static class CustomRedisHttpSessionConfiguration {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "${session.redis.namespace}", enableIndexingAndEvents = true)
static class CustomRedisHttpSessionConfiguration2 {
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class CustomRedisMessageListenerContainerConfig {
@Bean
RedisMessageListenerContainer redisMessageListenerContainer() {
return mock(RedisMessageListenerContainer.class);
}
}
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -26,6 +26,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
@@ -33,8 +34,10 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -75,6 +78,14 @@ class Gh109Tests {
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -20,7 +20,7 @@ ui:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
pipeline:
antora:
extensions:
- require: ./antora/extensions/version-fix.js
- require: ./antora/extensions/major-minor-segment.js
- require: ./antora/extensions/root-component-name.js

View File

@@ -1,3 +1,3 @@
name: ROOT
version: '3.0.0'
prerelease: '-M1'
version: '3.0.0-M3'
prerelease: 'true'

View File

@@ -3,8 +3,8 @@
const { posix: path } = require('path')
module.exports.register = (pipeline, { config }) => {
pipeline.on('contentClassified', ({ contentCatalog }) => {
module.exports.register = function({ config }) {
this.on('contentClassified', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(component => {
const componentName = component.name;
const generationToVersion = new Map();
@@ -197,4 +197,4 @@ function no_data(key, value) {
return value ? "__data__" : value;
}
return value;
}
}

View File

@@ -1,40 +0,0 @@
// https://gitlab.com/antora/antora/-/issues/132#note_712132072
'use strict'
const { posix: path } = require('path')
module.exports.register = (pipeline, { config }) => {
pipeline.on('contentClassified', ({ contentCatalog }) => {
const rootComponentName = config.rootComponentName || 'ROOT'
const rootComponentNameLength = rootComponentName.length
contentCatalog.findBy({ component: rootComponentName }).forEach((file) => {
if (file.out) {
file.out.dirname = file.out.dirname.substr(rootComponentNameLength)
file.out.path = file.out.path.substr(rootComponentNameLength + 1)
file.out.rootPath = fixPath(file.out.rootPath)
}
if (file.pub) {
file.pub.url = file.pub.url.substr(rootComponentNameLength + 1)
if (file.pub.rootPath) {
file.pub.rootPath = fixPath(file.pub.rootPath)
}
}
if (file.rel) {
if (file.rel.pub) {
file.rel.pub.url = file.rel.pub.url.substr(rootComponentNameLength + 1)
file.rel.pub.rootPath = fixPath(file.rel.pub.rootPath);
}
}
})
const rootComponent = contentCatalog.getComponent(rootComponentName)
rootComponent?.versions?.forEach((version) => {
version.url = version.url.substr(rootComponentName.length + 1)
})
// const siteStartPage = contentCatalog.getById({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' })
// if (siteStartPage) delete siteStartPage.out
})
function fixPath(path) {
return path.split('/').slice(1).join('/') || '.'
}
}

View File

@@ -0,0 +1,15 @@
'use strict'
module.exports.register = function({ config }) {
this.on('contentAggregated', ({ contentAggregate }) => {
contentAggregate.forEach(aggregate => {
if (aggregate.version === "2.6.2" &&
aggregate.prerelease == "-SNAPSHOT") {
aggregate.version = "2.6.2"
aggregate.displayVersion = `${aggregate.version}`
delete aggregate.prerelease
}
})
})
}

View File

@@ -20,7 +20,7 @@ ui:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
pipeline:
antora:
extensions:
- require: ./antora/extensions/version-fix.js
- require: ./antora/extensions/major-minor-segment.js
- require: ./antora/extensions/root-component-name.js

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -20,7 +20,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.session.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
@@ -28,6 +30,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -47,7 +52,20 @@ public class HttpSessionConfigurationNoOpConfigureRedisActionXmlTests {
}
static RedisConnectionFactory connectionFactory() {
return mock(RedisConnectionFactory.class);
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connectionMock).pSubscribe(any(), any());
return connectionFactoryMock;
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
@@ -113,6 +114,18 @@ class IndexDocTests {
}
// end::expire-repository-demo[]
@Test
@SuppressWarnings("unused")
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newRedisIndexedSessionRepository() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -21,13 +21,18 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -55,7 +60,20 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
@Bean
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connectionMock).pSubscribe(any(), any());
return connectionFactoryMock;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -26,6 +26,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
@@ -33,8 +34,10 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
@@ -67,6 +70,15 @@ public abstract class AbstractHttpSessionListenerTests {
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connection).pSubscribe(any(), any());
return factory;
}

View File

@@ -14,7 +14,7 @@
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration"/>
<bean class="docs.http.AbstractHttpSessionListenerTests"
factory-method="createMockRedisConnection"/>

View File

@@ -148,6 +148,78 @@ Note that no infrastructure for session expirations is configured for you.
This is because things such as session expiration are highly implementation-dependent.
This means that, if you require cleaning up expired sessions, you are responsible for cleaning up the expired sessions.
[[api-redissessionrepository]]
== Using `RedisSessionRepository`
`RedisSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
Note that this implementation does not support publishing of session events.
[[api-redissessionrepository-new]]
=== Instantiating a `RedisSessionRepository`
You can see a typical example of how to create a new instance in the following listing:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-redissessionrepository]
----
====
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redissessionrepository-config]]
=== Using `@EnableRedisHttpSession`
In a web environment, the simplest way to create a new `RedisSessionRepository` is to use `@EnableRedisHttpSession`.
You can find complete example usage in the xref:samples.adoc#samples[Samples and Guides (Start Here)].
You can use the following attributes to customize the configuration:
enableIndexingAndEvents
* *enableIndexingAndEvents*: Whether to use a `RedisIndexedSessionRepository` instead of a `RedisSessionRepository`. The default is `false`.
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds.
* *redisNamespace*: Allows configuring an application specific namespace for the sessions. Redis keys and channel IDs start with the prefix of `<redisNamespace>:`.
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `SessionRepository`.
A value of `FlushMode.IMMEDIATE` writes to Redis as soon as possible.
==== Custom `RedisSerializer`
You can customize the serialization by creating a bean named `springSessionDefaultRedisSerializer` that implements `RedisSerializer<Object>`.
[[api-redissessionrepository-cli]]
=== Viewing the Session in Redis
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
For example, you can enter the following command into a terminal window:
====
[source,bash]
----
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" <1>
----
<1> The suffix of this key is the session identifier of the Spring Session.
====
You can also view the attributes of each session by using the `hkeys` command.
The following example shows how to do so:
====
[source,bash]
----
redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"
----
====
[[api-redisindexedsessionrepository]]
== Using `RedisIndexedSessionRepository`
@@ -170,12 +242,13 @@ include::{indexdoc-tests}[tags=new-redisindexedsessionrepository]
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redisindexedsessionrepository-config]]
=== Using `@EnableRedisHttpSession`
=== Using `@EnableRedisHttpSession(enableIndexingAndEvents = true)`
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`.
You can find complete example usage in the xref:samples.adoc#samples[Samples and Guides (Start Here)].
You can use the following attributes to customize the configuration:
* *enableIndexingAndEvents*: Whether to use a `RedisIndexedSessionRepository` instead of a `RedisSessionRepository`. The default is `false`.
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds.
* *redisNamespace*: Allows configuring an application specific namespace for the sessions. Redis keys and channel IDs start with the prefix of `<redisNamespace>:`.
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `SessionRepository`.
@@ -335,7 +408,7 @@ redis-cli config set notify-keyspace-events Egx
----
====
If you use `@EnableRedisHttpSession`, managing the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
If you use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`, managing the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
However, in a secured Redis enviornment, the config command is disabled.
This means that Spring Session cannot configure Redis Keyspace events for you.
To disable the automatic configuration, add `ConfigureRedisAction.NO_OP` as a bean.

View File

@@ -235,7 +235,7 @@ To use this support, you need to:
* Configure `SessionEventHttpSessionListenerAdapter` as a Spring bean.
* Inject every `HttpSessionListener` into the `SessionEventHttpSessionListenerAdapter`
If you use the configuration support documented in <<httpsession-redis,`HttpSession` with Redis>>, all you need to do is register every `HttpSessionListener` as a bean.
If you use the Redis support with `enableIndexingAndEvents` set to `true`, `@EnableRedisHttpSession(enableIndexingAndEvents = true)`, all you need to do is register every `HttpSessionListener` as a bean.
For example, assume you want to support Spring Security's concurrency control and need to use `HttpSessionEventPublisher`. In that case, you can add `HttpSessionEventPublisher` as a bean.
In Java configuration, this might look like the following:

View File

@@ -55,7 +55,10 @@ Spring Session is Open Source software released under the https://www.apache.org
| Name | Location
| Spring Session Infinispan
| https://infinispan.org/infinispan-spring-boot/master/spring_boot_starter.html#_enabling_spring_session_support
| https://infinispan.org/docs/stable/titles/spring/spring.html
| Spring Session Caffeine
| https://github.com/gotson/spring-session-caffeine
|===

View File

@@ -29,7 +29,7 @@ dependencies {
}
antora {
antoraVersion = "3.0.0-alpha.8"
antoraVersion = "3.0.1"
arguments = ["--fetch"]
}

View File

@@ -728,7 +728,8 @@ public class JdbcIndexedSessionRepository
T attributeValue = supplier.get();
if (attributeValue != null
&& JdbcIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, DeltaValue.UPDATED);
this.delta.merge(attributeName, DeltaValue.UPDATED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : deltaValue);
}
return attributeValue;
}

View File

@@ -655,6 +655,20 @@ class JdbcIndexedSessionRepositoryTests {
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveWithSaveModeOnGetAttributeAndNewAttributeSetAndGet() {
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
JdbcSession session = this.repository.new JdbcSession(delegate, UUID.randomUUID().toString(), false);
session.setAttribute("attribute2", "value2");
session.getAttribute("attribute2");
this.repository.save(session);
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveWithSaveModeAlways() {
this.repository.setSaveMode(SaveMode.ALWAYS);

View File

@@ -9,7 +9,7 @@ dependencyManagement {
dependency 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:2.0.0'
dependency 'jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0'
dependency 'org.glassfish.web:jakarta.servlet.jsp.jstl:2.0.0'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.52.0'
dependency 'org.seleniumhq.selenium:htmlunit-driver:3.61.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.33'
dependency 'org.slf4j:log4j-over-slf4j:1.7.33'
dependency 'org.webjars:bootstrap:2.3.2'

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014-2022 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.mvc;
import java.security.Principal;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose security related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class SecurityControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
@ModelAttribute("httpSessionId")
String sessionId(HttpSession httpSession) {
return httpSession.getId();
}
}

View File

@@ -19,11 +19,11 @@ package sample.session;
import java.io.IOException;
import java.net.InetAddress;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.CityResponse;

View File

@@ -7,7 +7,7 @@
<h1>Secured Page</h1>
<p>This page is secured using Spring Boot, Spring Session, and Spring Security.</p>
<p>Your current session id is <span id="session-id" th:text="${#httpSession.id}"></span></p>
<p>Your current session id is <span id="session-id" th:text="${httpSessionId}"></span></p>
<table class="table table-stripped">
<tr>
@@ -21,12 +21,12 @@
<tr th:each="sessionElement : ${sessions}" th:with="details=${sessionElement.getAttribute('SESSION_DETAILS')}">
<td th:text="${sessionElement.id.substring(30)}"></td>
<td th:text="${details?.location}"></td>
<td th:text="${#temporals.format(sessionElement.creationTime.atZone(T(java.time.ZoneId).systemDefault()),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#temporals.format(sessionElement.lastAccessedTime.atZone(T(java.time.ZoneId).systemDefault()),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#dates.format(sessionElement.creationTime,'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#dates.format(sessionElement.lastAccessedTime,'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${details?.accessType}"></td>
<td>
<form th:action="@{'/sessions/' + ${sessionElement.id}}" th:method="post">
<input th:id="'terminate-' + ${sessionElement.id}" type="submit" value="Terminate" th:disabled="${sessionElement.id == #httpSession.id}"/>
<input th:id="'terminate-' + ${sessionElement.id}" type="submit" value="Terminate" th:disabled="${sessionElement.id == httpSessionId}"/>
</form>
</td>
</tr>

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2022 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.
@@ -16,16 +16,22 @@
package sample.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.security.Principal;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -14,18 +14,23 @@
* limitations under the License.
*/
package sample.config;
package sample;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* Controller for sending the user to the login view.
*
* @author Rob Winch
*
*/
@Controller
public class IndexController {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
@RequestMapping("/")
public String index() {
return "index";
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 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.security.Principal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose security related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -5,7 +5,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-webflux"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.springframework.boot:spring-boot-starter-data-mongodb-reactive"
implementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo"
implementation "org.testcontainers:mongodb"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.seleniumhq.selenium:htmlunit-driver"

View File

@@ -16,8 +16,18 @@
package org.springframework.session.mongodb.examples;
import java.util.HashMap;
import java.util.Map;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession;
/**
@@ -32,7 +42,38 @@ import org.springframework.session.data.mongo.config.annotation.web.reactive.Ena
public class SpringSessionMongoReactiveApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSessionMongoReactiveApplication.class);
SpringApplication application = new SpringApplication(SpringSessionMongoReactiveApplication.class);
application.addInitializers(new Initializer());
application.run(args);
}
/**
* Use Testcontainers to managed MongoDB through Docker.
* <p>
*
* @see <a href=
* "https://bsideup.github.io/posts/local_development_with_testcontainers/">Local
* Development with Testcontainers</a>
*/
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
static MongoDBContainer mongo = new MongoDBContainer(DockerImageName.parse("mongo:5.0"));
private static Map<String, String> getProperties() {
mongo.start();
HashMap<String, String> properties = new HashMap<>();
properties.put("spring.data.mongodb.host", mongo.getHost());
properties.put("spring.data.mongodb.port", mongo.getFirstMappedPort() + "");
return properties;
}
@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment env = context.getEnvironment();
env.getPropertySources().addFirst(new MapPropertySource("testcontainers", (Map) getProperties()));
}
}
}

View File

@@ -0,0 +1,2 @@
logging.level.org.springframework.data.mongodb=DEBUG
logging.level.org.springframework.session=DEBUG

View File

@@ -1,4 +0,0 @@
logging:
level:
org.springframework.data.mongodb: DEBUG
org.springframework.session: DEBUG

View File

@@ -26,9 +26,10 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.session.mongodb.examples.pages.HomePage;
import org.springframework.session.mongodb.examples.pages.HomePage.Attribute;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = SpringSessionMongoReactiveApplication.Initializer.class)
public class AttributeTests {
@LocalServerPort

View File

@@ -5,10 +5,11 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity6"
implementation "org.springframework.boot:spring-boot-starter-data-mongodb"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo"
implementation "org.testcontainers:mongodb"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.seleniumhq.selenium:htmlunit-driver"

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2014-2016 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 org.springframework.session.mongodb.examples;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
class EmbeddedMongoPortLogger implements ApplicationRunner, EnvironmentAware {
private static final Log logger = LogFactory.getLog(EmbeddedMongoPortLogger.class);
private Environment environment;
@Override
public void run(ApplicationArguments args) throws Exception {
String port = this.environment.getProperty("local.mongo.port");
logger.info("Embedded Mongo started on port " + port + ", use 'mongo --port " + port + "' command to connect");
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

View File

@@ -16,8 +16,18 @@
package org.springframework.session.mongodb.examples;
import java.util.HashMap;
import java.util.Map;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
/**
* @author Rob Winch
@@ -26,7 +36,38 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
public class SpringSessionMongoTraditionalBoot {
public static void main(String[] args) {
SpringApplication.run(SpringSessionMongoTraditionalBoot.class, args);
SpringApplication application = new SpringApplication(SpringSessionMongoTraditionalBoot.class);
application.addInitializers(new Initializer());
application.run(args);
}
/**
* Use Testcontainers to managed MongoDB through Docker.
* <p>
*
* @see <a href=
* "https://bsideup.github.io/posts/local_development_with_testcontainers/">Local
* Developmenet with Testcontainers</a>
*/
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
static MongoDBContainer mongo = new MongoDBContainer(DockerImageName.parse("mongo:5.0"));
private static Map<String, String> getProperties() {
mongo.start();
HashMap<String, String> properties = new HashMap<>();
properties.put("spring.data.mongodb.host", mongo.getHost());
properties.put("spring.data.mongodb.port", mongo.getFirstMappedPort() + "");
return properties;
}
@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment env = context.getEnvironment();
env.getPropertySources().addFirst(new MapPropertySource("testcontainers", (Map) getProperties()));
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 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 org.springframework.session.mongodb.examples.mvc;
import java.security.Principal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -1,3 +1,5 @@
spring.thymeleaf.cache=false
spring.template.cache=false
spring.data.mongodb.port=0
logging.level.org.springframework.data.mongodb=DEBUG
logging.level.org.springframework.session=DEBUG

View File

@@ -1,4 +1,4 @@
<html xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect" layout:decorator="layout">
<html xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect" layout:decorate="layout">
<head>
<title>Secured Content</title>
</head>

View File

@@ -3,7 +3,7 @@
xmlns:th="https://www.thymeleaf.org"
xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect">
<head>
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
<link rel="icon" type="image/x-icon" th:href="@{/resources/img/favicon.ico}" href="../static/img/favicon.ico"/>
<link th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"></link>
<style type="text/css">
@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/resources/img/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -33,6 +33,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.session.mongodb.examples.pages.HomePage;
import org.springframework.session.mongodb.examples.pages.LoginPage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
@@ -45,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@ContextConfiguration(initializers = SpringSessionMongoTraditionalBoot.Initializer.class)
public class BootTests {
@Autowired

View File

@@ -16,7 +16,7 @@
package sample.web;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import java.security.Principal;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class SecurityControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
@ModelAttribute("httpSession")
HttpSession httpSession(HttpSession httpSession) {
return httpSession;
}
}

View File

@@ -23,9 +23,9 @@
</tr>
</thead>
<tbody>
<tr th:each="name : ${T(java.util.Collections).list(#httpSession.getAttributeNames())}">
<tr th:each="name : ${T(org.springframework.util.CollectionUtils).toIterator(httpSession?.getAttributeNames())}">
<td th:text="${name}"></td>
<td th:text="${#httpSession.getAttribute(name)}"></td>
<td th:text="${httpSession.getAttribute(name)}"></td>
</tr>
</tbody>
</table>

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2014-2022 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.web.bind.annotation.GetMapping;
/**
* An index controller.
*
* @author Rob Winch
*/
@Controller
public class IndexController {
@GetMapping("/")
String index() {
return "index";
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 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.security.Principal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -86,13 +86,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out"/>
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2022 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.
@@ -16,16 +16,20 @@
package sample.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* An index controller.
*
* @author Rob Winch
*/
@Controller
public class IndexController {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
@GetMapping("/")
String index() {
return "index";
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
import java.security.Principal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>

View File

@@ -31,7 +31,7 @@ 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.boot.test.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;

View File

@@ -31,7 +31,7 @@ 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.boot.test.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;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -18,13 +18,13 @@ package sample.data;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -38,7 +38,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
*
* @author Rob Winch
*/
@Entity
@Entity(name = "custom_user")
public class User implements Serializable {
@Id

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 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.mvc;
import java.security.Principal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
/**
* {@link ControllerAdvice} to expose user related attributes.
*
* @author Rob Winch
*/
@ControllerAdvice
public class UserControllerAdvise {
@ModelAttribute("currentUserName")
String currentUser(Principal principal) {
return (principal != null) ? principal.getName() : null;
}
}

View File

@@ -1,5 +1,5 @@
insert into user(id,email,password,first_name,last_name) values (0,'rob','password','Rob','Winch');
insert into user(id,email,password,first_name,last_name) values (1,'luke','password','Luke','Taylor');
insert into user(id,email,password,first_name,last_name) values (2,'eve','password','Luke','Taylor');
insert into custom_user(id,email,password,first_name,last_name) values (0,'rob','password','Rob','Winch');
insert into custom_user(id,email,password,first_name,last_name) values (1,'luke','password','Luke','Taylor');
insert into custom_user(id,email,password,first_name,last_name) values (2,'eve','password','Luke','Taylor');
update user set password = '$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u';
update custom_user set password = '$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u';

View File

@@ -81,13 +81,12 @@
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<div class="nav-collapse collapse">
<div th:if="${currentUserName != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
<p id="un" class="navbar-text pull-right" th:text="${currentUserName}">
sample_user
</p>
</div>