Compare commits

..

247 Commits

Author SHA1 Message Date
Eleftheria Stein
e02a38965f Release 2.5.0-RC2 2021-10-20 12:56:36 +02:00
Greg L. Turnquist
bf139dbbb3 Introduce Spring Session MongoDB
* Migrate the module's code back into this project.
* Fold the documentation in.
* Update to current Gradle conventions.
* Reformat to match styling.
2021-10-20 11:57:27 +02:00
Eleftheria Stein
d10c18eb88 Next development version 2021-10-19 11:51:02 +02:00
Eleftheria Stein
8af09781a0 Release 2.6.0-RC1 2021-10-19 11:38:25 +02:00
Eleftheria Stein
845c7aca84 Upgrade test dependencies 2021-10-19 11:14:03 +02:00
Eleftheria Stein
b05575722c Upgrade samples to Spring Boot 2.5.5
Closes gh-1929
2021-10-19 10:54:01 +02:00
Eleftheria Stein
ee0e03b91e Upgrade Spring Security to 5.6.0-RC1
Closes gh-1928
2021-10-19 10:39:06 +02:00
Eleftheria Stein
7864f9c4cc Upgrade Spring Framework to 5.3.11
Closes gh-1927
2021-10-19 10:37:19 +02:00
Eleftheria Stein
227aee8e3a Upgrade Reactor to 2020.0.12
Closes gh-1925
2021-10-19 10:36:09 +02:00
Eleftheria Stein
bf2aaa0033 Upgrade Spring Data to 2021.1.0-RC1
Closes gh-1926
2021-10-19 10:35:39 +02:00
Eleftheria Stein
eb9f62a437 Update principal index on session ID change
Closes gh-1791
2021-10-14 17:49:56 +02:00
Rob Winch
418cb60f39 Add antora gradle plugin
You can now run the following to generate the antora site

./gradlew antora

It will appear at build/site/index.html
2021-10-11 09:17:50 -05:00
Rob Winch
4339b8ae9d Fix local-antora-playbook.yml
- Point to generated content
- Point to remote antora-ui-spring
2021-10-11 09:16:55 -05:00
Eleftheria Stein
63f706dbf9 Fix Hazelcast session with flush mode immediate
Closes gh-1921
2021-10-05 10:59:06 +02:00
Eleftheria Stein
beb7b334c4 Fix link to Spring Security remember-me docs
Closes gh-1915
2021-10-04 16:34:52 +02:00
Eleftheria Stein
a64a11ba03 Tests for Hazelcast flush mode immediate
Closes gh-1801
2021-10-01 17:02:33 +02:00
slondono
661ecaf371 Store Principal Name Index in the Hazelcast Session delta
Issue gh-1801
2021-10-01 17:02:33 +02:00
Rob Winch
378ba6db2c Use GH_ACTIONS_REPO_TOKEN 2021-09-27 13:22:20 -05:00
zhaokai
9659f1f571 Modify to support negative numbers 2021-09-27 14:40:16 +02:00
Eleftheria Stein
919a2a5c49 Upgrade back to Spring Boot 2.5.3 2021-09-24 16:46:32 +02:00
Rob Winch
4dee8063c6 Use Antora
Closes gh-1237
2021-09-23 16:44:39 -05:00
Eleftheria Stein
9ad871a30b Add setter for autowired field in SpringWebSessionConfiguration
Closes gh-1918
2021-09-23 14:54:22 +02:00
Eleftheria Stein
e7d58f6b03 Increase session timeout in Hazelcast tests
It's possible that the session is expiring before the assertions can be made in the tests, causing them to fail.

Issue gh-1912
2021-09-08 12:04:26 +02:00
Rob Winch
3d118242ee Better hiearchy with Samples nav 2021-08-31 10:38:54 -05:00
Rob Winch
0c00ff0598 Fix Samples nav 2021-08-30 19:12:50 -05:00
Rob Winch
3d93bfc28b Fix Boot Samples Nav 2021-08-30 19:08:46 -05:00
Rob Winch
297ff83775 Added missing versions 2021-08-30 19:07:17 -05:00
Rob Winch
1fc2c430f1 Fix antora name 2021-08-30 19:01:52 -05:00
Rob Winch
5757e94658 Generated antora.yml 2021-08-30 18:55:11 -05:00
Rob Winch
8cc22a1712 Use versionless URL 2021-08-30 17:17:34 -05:00
Vedran Pavic
79fbca24eb Make Hazelcast session repository bean factory return type more specific
The declared return type of Hazelcast session repository bean factory method (i.e. HazelcastHttpSessionConfiguration#sessionRepository) was changed to SessionRepository<?> when support for Hazelcast 4 was added. This breaks Spring Boot's ability to auto-configure sessions endpoint, which is @ConditionalOnBean(FindByIndexNameSessionRepository.class), as the current return type is not specific enough to satisfy this condition.

This commit changes the return type of Hazelcast session repository bean factory method to FindByIndexNameSessionRepository<?>.

Closes: gh-1905
2021-08-27 01:51:55 +02:00
Vedran Pavic
5b7aee7199 Fix Spring Boot based Hazelcast samples
This commit removes unused Hazelcast client dependencies and test support from Spring Boot based Hazelcast samples.

Closes: gh-1902
2021-08-27 00:46:23 +02:00
Andreas Kasparek
c5bffde790 Always set time-to-live within entry processor
Closes gh-1899
2021-08-25 13:16:08 +02:00
Rob Winch
aee65ffec8 Remove :toc: left
This causes an extra toc that covers the left navigation
2021-08-20 14:29:21 -05:00
Rob Winch
00abd345ac Add Dispatch to build reference 2021-08-18 11:27:45 -05:00
Rob Winch
0864140dda Clean up introduction 2021-08-18 11:19:21 -05:00
Rob Winch
7babddf15f Fix default xref text 2021-08-18 11:16:36 -05:00
Rob Winch
764fc4eea6 <<>> to xref 2021-08-18 11:15:27 -05:00
Rob Winch
26419e2149 Cleanup Antora 2021-08-18 11:10:33 -05:00
Eleftheria Stein
585d3695ad Point to spring-session tag in GitHub issue template
Issue: gh-1897
2021-08-18 14:06:12 +02:00
Eleftheria Stein
db8a3aa604 Next development version 2021-08-17 15:52:29 +02:00
Eleftheria Stein
d0fabc0a35 Release 2.6.0-M1 2021-08-17 15:50:52 +02:00
Eleftheria Stein
cae8b51eab Upgrade test dependencies 2021-08-17 15:14:03 +02:00
Eleftheria Stein
2236449635 Revert "Upgrade test dependencies"
This reverts commit 9fde87c11b.
2021-08-17 14:07:13 +02:00
Eleftheria Stein
d862836d41 Upgrade Hazelcast 4 to 4.2.2
Closes gh-1887
2021-08-17 13:45:48 +02:00
Eleftheria Stein
4008afe47b Upgrade Spring Security to 5.6.0-M2
Closes gh-1884
2021-08-17 13:29:58 +02:00
Eleftheria Stein
9fde87c11b Upgrade test dependencies 2021-08-17 13:23:08 +02:00
Rob Winch
faa6c441fa Antora 2021-08-16 15:44:15 -05:00
Eleftheria Stein
93c62104ee Make Websocket sample compatible with Spring Boot 2.5.3
Closes gh-1886
2021-08-16 15:08:38 +02:00
Eleftheria Stein
8fdcfc28bc Upgrade samples to Spring Boot 2.5.3
Closes gh-1885
2021-08-16 15:08:09 +02:00
Eleftheria Stein
1f6445999f Upgrade Spring Framework to 5.3.9
Closes gh-1882
2021-08-16 14:00:02 +02:00
Eleftheria Stein
cf4aeae02a Upgrade Spring Data to 2021.1.0-M2
Closes gh-1883
2021-08-16 13:59:25 +02:00
Eleftheria Stein
f8dcee7304 Upgrade Reactor to 2020.0.10
Closes gh-1881
2021-08-16 13:57:00 +02:00
Eleftheria Stein
971a2d17d9 Set cookie path to context path without trailing slash
Closes gh-1863
2021-07-20 10:00:53 +02:00
Ellie Bahadori
8b5b3701da Prevent builds from running on forks
Resolves gh-1678
2021-07-06 17:19:30 +02:00
Eleftheria Stein
21c9fb0cfa Point "What's New" section to Wiki
Closes gh-1878
2021-06-29 16:08:03 +02:00
Jay Bryant
33993b2ff6 Add links to Mongo and Geode docs
Spring Session for MongoDB and Geodo have their own reference guides.
This PR adds links to them.
2021-06-14 13:34:36 +02:00
Jay Bryant
9f4a723160 Add links to the other documentation format
Add a link from the HTML to the PDF and vice-versa.
2021-06-04 08:55:57 +02:00
Eleftheria Stein
25032fbd61 Use GPG_PRIVATE_KEY directly in build
Closes gh-1861
2021-05-31 15:42:16 +03:00
Eleftheria Stein
d195579ced Next development version 2021-05-19 00:41:23 +02:00
Eleftheria Stein
b1d68c0731 Release 2.5.0 2021-05-19 00:13:33 +02:00
Eleftheria Stein
7ec5add1bd Configure user name for Gradle CI builds
This reverts commit e41ebd8a77.
2021-05-19 00:13:17 +02:00
Eleftheria Stein
05e103d9c5 Use GPG_PRIVATE_KEY_NO_HEADER secret in CI
Issue gh-1812
2021-05-19 00:09:26 +02:00
Eleftheria Stein
8190072d3f Revert "Release 2.5.0"
This reverts commit ce16374c15.
2021-05-19 00:08:02 +02:00
Eleftheria Stein
ce16374c15 Release 2.5.0 2021-05-18 22:24:46 +02:00
Eleftheria Stein
e41ebd8a77 Revert "Configure user name for Gradle CI builds"
This reverts commit ce938fd2fe.
2021-05-18 22:22:37 +02:00
Eleftheria Stein
8550aeca5c Revert "Release 2.5.0"
This reverts commit 5cb8a6b79a.
2021-05-18 22:22:21 +02:00
Eleftheria Stein
5cb8a6b79a Release 2.5.0 2021-05-18 13:28:56 +02:00
Eleftheria Stein
5b48e7e8e7 Upgrade Spring Security to 5.5.0
Closes gh-1856
2021-05-18 10:00:37 +02:00
Eleftheria Stein
9e2b729d62 Upgrade test dependencies 2021-05-17 17:45:47 +02:00
Eleftheria Stein
524ee0d9bc Upgrade samples to Spring Boot 2.4.5
Closes gh-1855
2021-05-17 17:20:46 +02:00
Eleftheria Stein
26be3218fb Upgrade Spring Framework to 5.3.7
Closes gh-1853
2021-05-17 17:12:53 +02:00
Eleftheria Stein
8d4fd80add Upgrade Spring Data to 2021.0.1
Closes gh-1854
2021-05-17 17:12:25 +02:00
Eleftheria Stein
6969ea0049 Upgrade Reactor to 2020.0.7
Closes gh-1852
2021-05-17 17:11:01 +02:00
Eleftheria Stein
ce938fd2fe Configure user name for Gradle CI builds
Closes gh-1851
2021-05-14 12:32:29 +02:00
weix sun
98d7448b40 Fix broken Framework link in reference docs 2021-05-11 11:18:52 +02:00
Eleftheria Stein
4bb2bd6fda JDBC session with negative timeout should never expire
Closes gh-1847
2021-05-10 16:38:30 +02:00
Eleftheria Stein
0e5dd1863f Rename master branch to main
Closes gh-1846
2021-04-28 17:19:52 +02:00
Eleftheria Stein
548b58ee55 Next development version 2021-04-13 12:43:12 +02:00
Eleftheria Stein
bb28af9934 Release 2.5.0-RC1 2021-04-13 12:28:27 +02:00
Eleftheria Stein
dee8402473 Upgrade test dependencies 2021-04-13 12:12:17 +02:00
Eleftheria Stein
7bd0b45f29 Upgrade samples to Spring Boot 2.4.4
Closes gh-1822
2021-04-13 12:01:37 +02:00
Eleftheria Stein
b42b01af9b Upgrade Hazelcast 4 to 4.2
Closes gh-1821
2021-04-13 11:31:58 +02:00
Eleftheria Stein
6744fee3cb Upgrade Hazelcast to 3.12.12
Closes gh-1820
2021-04-13 11:22:00 +02:00
Eleftheria Stein
6811f25565 Upgrade Spring Security to 5.5.0-RC1
Closes gh-1819
2021-04-13 11:21:10 +02:00
Eleftheria Stein
47817c46e1 Upgrade Spring Data to 2021.0.0-RC1
Closes gh-1818
2021-04-13 11:20:20 +02:00
Eleftheria Stein
4f7c6406ad Upgrade Spring Framework to 5.3.5
Closes gh-1817
2021-04-13 11:19:27 +02:00
Eleftheria Stein
34cc1d1171 Upgrade Reactor to 2020.0.6
Closes gh-1816
2021-04-13 11:18:29 +02:00
Josh Cummings
9e7d9912e5 Change to GPG_PRIVATE_KEY_NO_HEADER
Closes gh-1812
2021-03-30 13:32:28 -06:00
Eleftheria Stein
d2960b570f Polish gh-1798
- Should throw IllegalStateException
2021-03-25 09:55:32 +02:00
Eleftheria Stein
8bd4374909 Throw exception if session created after response
Closes gh-1798
2021-03-25 09:27:27 +02:00
Eleftheria Stein
15f29f8adf Fix README formatting 2021-03-22 12:48:34 +02:00
Eleftheria Stein
74d53e8bfc Add additional information to README 2021-03-22 12:45:51 +02:00
Eleftheria Stein
77deb63373 Fix SessionRepositoryFilter Javadoc 2021-03-17 16:39:45 +01:00
Stefan Wurzinger
69285f2a9a Fix Redis session expiration entry deletion
Closes gh-585
2021-03-12 16:24:48 +01:00
Eleftheria Stein
c93513f18f Fix typo in reference docs 2021-02-09 16:12:51 +01:00
Eleftheria Stein
27044c8766 Make Hazelcast tests independent of ordering
Closes gh-1787
2021-02-02 13:24:24 +01:00
Eleftheria Stein
b198844671 Use io.spring.gradle-enterprise-conventions
Adds support for build cache and build scans

Closes gh-1713
2021-02-01 18:14:28 +01:00
Eleftheria Stein
0f27bbaff7 Update to gradle 6.8.1
Closes gh-1785
2021-02-01 17:32:09 +01:00
Eleftheria Stein
62ad3e1bab Replace deprecated verifyZeroInteractions in tests 2021-01-29 17:00:03 +01:00
Eleftheria Stein
7eed8427a5 Use spring-build-conventions:0.0.37 2021-01-27 11:09:43 +01:00
Eleftheria Stein
4cbb253c11 Resolve artifacts from Maven Central first
- Use spring-build-conventions:0.0.36
- Add https://repo.spring.io/release to reference

Closes gh-1778
2021-01-22 15:27:06 +01:00
Eleftheria Stein
3fe03c60f3 Add manual trigger to CI workflow
Closes gh-1777
2021-01-19 17:37:18 +01:00
Eleftheria Stein
d95652dcb3 Next development version 2021-01-19 14:19:49 +01:00
Eleftheria Stein
cfc1a1e7ce Release 2.5.0-M1 2021-01-19 13:55:51 +01:00
Eleftheria Stein
e17d0cc1d9 Upgrade test dependencies 2021-01-18 12:54:54 +01:00
Eleftheria Stein
0a0766e4a8 Upgrade Hazelcast 4 to 4.1.1
Closes gh-1761
2021-01-18 12:43:34 +01:00
Eleftheria Stein
4108c77797 Upgrade Hazelcast to 3.12.11
Closes gh-1760
2021-01-18 12:41:02 +01:00
Eleftheria Stein
c015a69a4a Upgrade Spring Data to 2021.0.0-M2
Closes gh-1759
2021-01-18 12:39:30 +01:00
Eleftheria Stein
293cf3f730 Upgrade Spring Security to 5.5.0-M1
Closes gh-1758
2021-01-18 12:37:49 +01:00
Eleftheria Stein
6f79e87c8f Upgrade Spring Framework to 5.3.3
Closes gh-1757
2021-01-18 12:35:29 +01:00
Eleftheria Stein
d74c5b1445 Upgrade Reactor to 2020.0.3
Closes gh-1755
2021-01-18 12:34:01 +01:00
Eleftheria Stein
6075089691 Upgrade samples to Spring Boot 2.4.2
Closes gh-1756
2021-01-18 12:31:54 +01:00
Eleftheria Stein
e7a0924904 Upgrade test dependencies 2021-01-12 13:34:47 +01:00
daisuzz
319f0a97ad Fix example in RedisIndexedSessionRepository and Storage Details section
* Fix example in JavaDocs of RedisIndexedSessionRepository

* Fix example in Storage Details section of documentation
2021-01-04 09:32:44 +01:00
Eleftheria Stein
95de199aa4 Upgrade samples to Spring Boot 2.4.1
Closes gh-1744
2020-12-14 09:56:17 +01:00
Eleftheria Stein
db589b7c29 Update community extensions section of reference manual
Closes gh-1736
2020-12-09 13:02:30 +01:00
Vedran Pavic
2aae51b1a1 Rationalize JDBC integration tests
This commit reduces the JDBC integration tests to only single (latest) version per RDBMS vendor, due to a growing number of integration tests. Additionally, the configuration of most containers is simplified due to improved defaults within the Testcontainers library.
2020-11-28 00:26:17 +01:00
Vedran Pavic
e721efeb85 Optimize insert attribute statement in JdbcIndexedSessionRepository
At present, the SQL statement used to insert a session attribute record contains a nested select statement that verifies the existence of parent record in the session table. Such approach can be susceptible to deadlocks on certain RDMBSs.

This commit optimizes the SQL statement used to insert session attribute so that it doesn't perform a nested select statement.

Closes: #1550
2020-11-28 00:26:17 +01:00
Vedran Pavic
0111c6e686 Provide database specific JdbcIndexedSessionRepository customizers
This commit provides JdbcIndexedSessionRepository customizers for the following SQL dialects:

- PostgreSQL
- MySQL (also used by MariaDB)
- SQL Server
- IBM DB2
- Oracle

These customizers are intended to address the concurrency issues occurring on insert of new session attribute by applying SQL dialect specific SQL upsert/merge statement instead of a generic insert.

Closes: #1213
2020-11-27 23:54:03 +01:00
Eleftheria Stein
07058c0cdf Add artifactory credentials to build 2020-11-17 13:59:06 +01:00
Thomas Heigl
5f5168814d Delay allocating Strings for message channel and body 2020-11-10 08:03:57 -05:00
Vedran Pavic
55502f336d Harmonize Redis key namespace configurations
At present, the RedisSessionRepository#setKeyNamespace expects users to provide the trailing colon (:) character that is used as separator between namespace segments. This is unlike RedisIndexedSessionRepository and ReactiveRedisSessionRepository that apply the separator implicitly in their respective #setRedisKeyNamespace methods.

This commit harmonizes the Redis key namespaces configurations across all Redis-backed session repository implementations.
2020-11-10 06:55:19 -05:00
Eleftheria Stein
0e83e3f1e0 Next development version 2020-11-10 11:48:27 +01:00
Eleftheria Stein
34876397a0 Next development version 2020-11-09 16:49:29 +01:00
Eleftheria Stein
faee8f1bdb Release 2.4.1 2020-11-09 15:40:43 +01:00
Eleftheria Stein
859784fe9e Use secrets from GitHub Actions workflow 2020-11-09 15:39:54 +01:00
Eleftheria Stein
4dd2db32d2 Revert "Release 2.4.1"
This reverts commit ae86831821.
2020-11-09 15:39:20 +01:00
Eleftheria Stein
ae86831821 Release 2.4.1 2020-11-04 17:36:47 +01:00
Eleftheria Stein
b722b12327 Fix formatting
Issue gh-1654
2020-10-30 14:34:37 +01:00
Kohei Tamura
29ff2e47fb Add try-with-resources to methods to insert BLOB 2020-10-30 08:45:52 -04:00
Eleftheria Stein
dc9da1d5bf Use OSSRH token credentials in workflow
Closes gh-1725
2020-10-30 13:42:36 +01:00
Eleftheria Stein
5a52df37ba Next development version 2020-10-28 23:36:40 +01:00
Eleftheria Stein
6d161575d5 Release 2.4.0wq 2020-10-28 22:48:46 +01:00
Eleftheria Stein
1cd8849eb9 Revert "Delete Jenkinsfile"
This reverts commit 68f867b60b.
2020-10-28 22:42:47 +01:00
Eleftheria Stein
cb3894614a Revert "Release 2.4.0"
This reverts commit 82e71d834b.
2020-10-28 22:42:06 +01:00
Eleftheria Stein
82e71d834b Release 2.4.0 2020-10-28 18:40:09 +01:00
Eleftheria Stein
81a9e71a5b Upgrade test and sample dependencies
This is needed in order for them to work with Spring Data 2020.0.0
2020-10-28 18:18:45 +01:00
Eleftheria Stein
298f0d59a0 Upgrade Spring Data to 2020.0.0
Closes gh-1721
2020-10-28 18:18:15 +01:00
Eleftheria Stein
c354284616 Upgrade samples to Spring Boot 2.4.0-M4
Closes gh-1722
2020-10-28 18:17:54 +01:00
Eleftheria Stein
4086044c2f Upgrade Spring Framework to 5.3.0
Closes gh-1720
2020-10-28 09:51:48 +01:00
Eleftheria Stein
e663401ecb Upgrade Hazelcast to 3.12.10
Closes gh-1718
2020-10-27 15:50:35 +01:00
Eleftheria Stein
60151c9e7d Upgrade Spring Security to 5.4.1
Closes gh-1717
2020-10-27 15:50:16 +01:00
Eleftheria Stein
18052460c6 Upgrade Reactor to 2020.0.0
Closes gh-1716
2020-10-27 15:49:49 +01:00
Eleftheria Stein
5092e86306 Upgrade samples to Spring Boot 2.3.4.RELEASE
Closes gh-1719
2020-10-27 15:49:19 +01:00
Eleftheria Stein
6de6df6dab Upgrade test dependencies 2020-10-27 15:15:30 +01:00
Vedran Pavic
301e65c2b9 Remove unnecessary Redis commands in RedisIndexedSessionRepository#save
See: #1331
2020-10-12 10:13:38 -04:00
Vedran Pavic
090a10fb10 Improve RedisSessionRepository-based sample configuration 2020-10-12 03:23:21 -04:00
Eleftheria Stein
235801487e Hazelcast4SessionUpdateEntryProcessor does not implement Offloadable
Closes gh-1707
2020-09-25 10:31:54 +02:00
Eleftheria Stein
e6e02de210 Upgrade Hazelcast 4 to 4.0.3
Closes gh-1706
2020-09-22 11:12:58 +02:00
Eleftheria Stein
b3b46fd8eb Upgrade Hazelcast to 3.12.9
Closes gh-1705
2020-09-22 10:46:15 +02:00
Eleftheria Stein
e46610f53a Next development version 2020-09-16 18:39:21 +02:00
Eleftheria Stein
e8c6b8db7b Release 2.4.0-RC1 2020-09-16 18:21:15 +02:00
Eleftheria Stein
486d00e5da Upgrade Spring Data to 2020.0.0-RC1
Closes gh-1704
2020-09-16 18:00:54 +02:00
Eleftheria Stein
0ab781e537 Consolidate Hazelcast configurations
Issue gh-1584
2020-09-16 16:35:03 +02:00
Eleftheria Stein
849b353cec Disable parallel deployment in CI build
Closes gh-1699
2020-09-16 09:59:38 +02:00
Eleftheria Stein
b262c9a3fd Upgrade Spring Framework to 5.3.0-RC1
Closes gh-1698
2020-09-15 17:18:35 +02:00
Eleftheria Stein
5d9e7caff0 Upgrade samples to Spring Boot 2.3.3.RELEASE
Closes gh-1683
2020-09-14 20:06:59 +02:00
Eleftheria Stein
dd348bc7b8 Upgrade test dependencies 2020-09-14 20:06:50 +02:00
Eleftheria Stein
9372986f84 Upgrade Spring Security to 5.4.0
Closes gh-1682
2020-09-14 19:41:49 +02:00
Eleftheria Stein
657c6a63e1 Upgrade Reactor to 2020.0.0-RC1
Closes gh-1681
2020-09-14 19:41:03 +02:00
Eleftheria Stein
a9c2336482 Use controller in Spring Boot sample
Issue gh-1647
2020-09-14 19:04:54 +02:00
Eleftheria Stein
068ed8d355 Ensure Hazelcast 4 compatibility with Java 9+ 2020-09-14 18:25:36 +02:00
Eleftheria Stein
2b6489c2bd Add support for Hazelcast 4
Closes gh-1584
2020-09-14 17:59:35 +02:00
Eleftheria Stein
c0c672b9f8 Update samples module link
Closes gh-1680
2020-09-09 17:04:37 +02:00
Ellie Bahadori
46d1205ff9 Create sample Spring Boot / Hazelcast project
Closes gh-1647
2020-09-09 15:44:31 +02:00
Enes Ozcan
cc85e927cd Add optional Hazelcast session serializer
Issue gh-1131
2020-09-08 07:31:32 -04:00
Ellie Bahadori
0819988a15 Move Gradle enterprise cache secrets to top level of CI build 2020-09-07 07:43:20 -04:00
Ellie Bahadori
0f3ea33b50 Fix indentation for cron job 2020-08-06 04:41:52 -04:00
Ellie Bahadori
0205c318d1 Remove placeholder comment from pipeline file 2020-08-04 05:18:40 -04:00
Ellie Bahadori
13bc1a5d24 Merge pull request #1663 from spring-projects/deploy-pipeline-test
Deploy pipeline test
2020-07-30 16:09:13 -07:00
Ellie Bahadori
8d2ec1ea44 Bring back master branch in preparation for merge
This reverts commit b54fb41952.
2020-07-30 14:53:18 -07:00
Ellie Bahadori
729ce13390 Add Gradle enterprise cache values to build steps 2020-07-30 14:34:37 -07:00
Ellie Bahadori
b54fb41952 Temporarily revert branch name changes to test artifact output
This reverts commit cf911322c2.
2020-07-30 14:10:10 -07:00
Ellie Bahadori
cf911322c2 Update badge to point to master and trigger builds on push to master branch 2020-07-28 11:46:38 -07:00
Ellie Bahadori
6bce5ddf7f Bump spring-build-conventions version and add README badge 2020-07-28 11:43:49 -07:00
Ellie Bahadori
7384504871 Fix YAML spacing issue 2020-07-27 12:14:12 -07:00
Ellie Bahadori
c21fff1a00 Add cron job back in 2020-07-27 11:57:36 -07:00
Ellie Bahadori
d602880a58 Re-introduce JDK matrix for CI pipeline 2020-07-27 11:00:01 -07:00
Ellie Bahadori
2a2c430793 Add URL for maven snapshots 2020-07-27 09:44:26 -07:00
Ellie Bahadori
6080611d1d Bump back up to 0.0.34.BUILD-SNAPSHOT 2020-07-27 09:44:26 -07:00
Ellie Bahadori
38adaeca94 Rev spring build conventions down to 0.0.33.RELEASE 2020-07-27 09:44:26 -07:00
Ellie Bahadori
6a791651e0 Bump spring build conventions version to 0.0.34.BUILD-SNAPSHOT 2020-07-27 09:44:26 -07:00
Ellie Bahadori
dfd6a0bc1b Add in deploy artifacts and docs steps 2020-07-27 09:44:26 -07:00
Ellie Bahadori
805820eeea Remove JDK version matrix for now 2020-07-27 09:44:26 -07:00
Ellie Bahadori
68f867b60b Delete Jenkinsfile 2020-07-27 09:44:26 -07:00
Ellie Bahadori
1044621caf Setup initial CI pipeline file 2020-07-27 09:44:26 -07:00
Eleftheria Stein-Kousathana
13f5cb4bac Document @SpringSessionDataSource in reference docs
Issue gh-1011
2020-07-27 12:14:41 +02:00
Thanh Nhan
5c05970b86 Update OncePerRequestFilter to match with spring-web
Closes gh-1658
2020-07-27 03:58:21 -04:00
Eleftheria Stein-Kousathana
0cd0bfb32f Remove attribute key and value from Redis
Closes gh-1331
2020-07-24 12:55:26 +02:00
Ellie Bahadori
b219806d8e Set up Github Actions pipeline for PRs 2020-07-23 04:01:58 -04:00
Eleftheria Stein
0f2a331ea3 Remove JDK 9 and 10 from Jenkins build
Closes gh-1659
2020-07-16 10:44:26 +02:00
Jay Bryant
ef8f667e35 Wording changes
Replacing some terms
2020-07-16 04:21:23 -04:00
Eleftheria Stein
4599e75c3a Next development version 2020-06-26 18:49:34 +02:00
Eleftheria Stein
8a971b9ce1 Release 2.4.0-M1 2020-06-26 18:25:57 +02:00
Eleftheria Stein
56e9dcfe20 Upgrade Spring Data to 2020.0.0-M1
Closes gh-1648
2020-06-26 11:45:17 -04:00
Eleftheria Stein
59e2cdb74f Upgrade Spring Framework to 5.3.0-M1
Closes gh-1649
2020-06-26 11:45:17 -04:00
Eleftheria Stein
847433562e Upgrade samples to Spring Boot 2.3.1.RELEASE
Closes gh-1650
2020-06-25 21:47:59 +02:00
Eleftheria Stein
55a6967331 Upgrade sample dependencies 2020-06-25 21:13:55 +02:00
Eleftheria Stein
2c8ce67ffc Upgrade Spring Security to 5.3.3.RELEASE
Closes gh-1651
2020-06-25 16:16:57 +02:00
Eleftheria Stein
076ed5cd71 Upgrade Reactor to Dysprosium-SR9
Closes gh-1652
2020-06-25 16:14:49 +02:00
Eleftheria Stein
f1ea71e55e Upgrade test dependencies 2020-06-25 15:41:39 +02:00
Eleftheria Stein
5acb307a54 Upgrade documentation styling
Resolves gh-1640
2020-05-14 16:12:06 -04:00
Eleftheria Stein
f921c4f527 Next development build 2020-05-12 14:41:38 -04:00
Eleftheria Stein
12dc76ec36 Release 2.3.0.RELEASE 2020-05-12 13:05:08 -04:00
Eleftheria Stein
7be3d30981 Upgrade Spring Security to 5.3.2.RELEASE
Resolves gh-1625
2020-05-12 12:58:59 -04:00
Eleftheria Stein
9c8fe23789 Upgrade Spring Data to Neumann-RELEASE
Resolves gh-1623
2020-05-12 12:58:12 -04:00
Eleftheria Stein
3114ef51ec Upgrade samples to Spring Boot 2.2.7
Resolves gh-1624
2020-05-12 12:54:19 -04:00
Kacper
9e7736bf7f Complete Javadoc description of setCookieMaxAge
Issue: gh-1627
2020-05-11 15:16:40 -04:00
Eleftheria Stein
6c5e335568 Upgrade Reactor to Dysprosium-SR7
Resolves gh-1626
2020-05-05 11:19:49 -04:00
Eleftheria Stein
1deedad3b9 Upgrade Spring Framework to 5.2.6.RELEASE
Resolves gh-1622
2020-05-05 11:17:34 -04:00
Eleftheria Stein
e4a8a6aa5c Upgrade test dependencies 2020-05-01 16:46:06 -04:00
Eleftheria Stein
49375a28fa Add guide for customizing cookie in WebFlux
Resolves gh-1614
2020-04-28 16:25:40 -04:00
Eleftheria Stein
5375f51bca Fix broken links in guides
Resolves gh-1621
2020-04-28 14:25:47 -04:00
Eleftheria Stein
29af9d3a4d WebFlux custom cookie sample
Resolves gh-1620
2020-04-22 12:40:40 -04:00
Eleftheria Stein
997ff56c63 Update gitignore 2020-04-22 12:40:40 -04:00
Rob Winch
06d8031211 Add status: waiting-for-triage to issue templates 2020-04-16 16:07:46 -05:00
Rob Winch
904369ac29 Revert PULL_REQUEST_TEMPLATE
Issue gh-1618
2020-04-15 20:34:52 -05:00
Rob Winch
266854a0be Add GitHub Issue Templates
Closes gh-1618
2020-04-15 20:22:29 -05:00
Rob Winch
8f02c83e06 Use GitHub default community health files
Closes gh-1617
2020-04-15 20:22:29 -05:00
Jay Bryant
570a7686b1 Fix a bad typo
Caught an egregious typing error from my own earlier work.
2020-04-15 16:38:40 -04:00
Rob Winch
fed318abc7 Find by Username Sample switch from DELETE to POST
Spring Boot 2.2 no longer adds HiddenHttpMethodFilter by default See
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.2-Release-Notes#httphiddenmethodfilter-disabled-by-default
This means that trying to map DELETE requests using _method variable
does not work.

This changes the mapping to use a POST which doesn't require the
HiddenHttpMethodFilter which might expose the application to unnecessary
security risk by allowing the HTTP method to be overridden.

Closes gh-1613
2020-04-13 09:41:02 -05:00
Eleftheria Stein
a824edd1c3 Mention Spring Boot implementation detection in docs
Resolves: gh-1610
2020-03-30 13:54:07 -04:00
慕华
aa4f783b45 Update boot-redis.adoc
on-save -> on_save
2020-03-16 08:08:14 -05:00
Eleftheria Stein
11fb68444f Fix invalid reference in docs 2020-03-13 11:04:12 -04:00
Eleftheria Stein
00026a30f4 Fix PDF docs
Resolves: #1603
2020-03-13 10:59:40 -04:00
Eleftheria Stein
c007437bd3 Next Development Build 2020-03-02 17:24:40 -05:00
Eleftheria Stein
dda13b5619 Release 2.3.0.RC1 2020-03-02 17:02:13 -05:00
Eleftheria Stein
366f13bd25 Upgrade Hazelcast to 3.12.6
Resolves: #1591
2020-03-02 16:29:04 -05:00
Eleftheria Stein
3535137c47 Upgrade test dependencies 2020-03-02 16:28:51 -05:00
Eleftheria Stein
a9bca9088f Upgrade Reactor to Dysprosium-SR5
Resolves: #1590
2020-03-02 16:14:41 -05:00
Eleftheria Stein
31de86ecef Upgrade samples to Spring Boot 2.2.5
Resolves: #1589
2020-03-02 15:46:30 -05:00
Eleftheria Stein
d123960f89 Upgrade Spring Data to Neumann-M3
Resolves: #1588
2020-03-02 15:45:19 -05:00
Eleftheria Stein
16d2923efd Upgrade Spring Security to 5.3.0.RC1
Resolves: #1587
2020-03-02 15:44:49 -05:00
Eleftheria Stein
24015d0854 Upgrade Spring Framework to 5.2.4.RELEASE
Resolves: #1586
2020-03-02 15:43:43 -05:00
Eleftheria Stein
d8f160c178 Update documentation styling
Upgrade spring-build-conventions to 0.0.28.RELEASE

Resolves: #1585
2020-03-02 15:30:29 -05:00
Eleftheria Stein
0318f6e2c1 Fix asciidoctor warnings
Invalid references and mismatched nesting blocks
2020-03-02 09:13:17 -05:00
Eleftheria Stein
43dd571345 Fix typo in Javadoc 2020-02-27 16:32:29 -05:00
Adam Kucera
e7fb9fce47 Fix examples in JavaDocs of classes which use SessionRepositories
The examples in JavaDocs of @EnableSpringHttpSession, SpringHttpSessionConfiguration and @EnableSpringWebSession were creating MapSessionRepository / ReactiveMapSessionRepository
using a constructor, which no longer exists in the classes. This should allow the example
to be used out of the box.
2020-02-20 15:25:46 -05:00
Eleftheria Stein
f13eb8d73e Use Spring Security lambda DSL in samples
Fixes: gh-1580
2020-02-19 12:36:51 +01:00
Jivko Vantchev
1a07ba5114 Fixes the duplicate index name in the example SQL script
The change is in the comments for the JdbcIndexedSessionRepository.
2020-02-14 12:03:24 +01:00
Eleftheria Stein
7125aac567 Next Development Build 2020-01-29 22:18:06 +01:00
Eleftheria Stein
3cbd3a9e25 Release 2.3.0.M1 2020-01-29 21:54:34 +01:00
Eleftheria Stein
4c914d46c9 Upgrade Spring Framework to 5.2.3.RELEASE
Resolves: #1575
2020-01-29 21:47:32 +01:00
Eleftheria Stein
adf411ecc3 Upgrade Spring Security to 5.3.0.M1
Resolves: #1568
2020-01-29 21:12:50 +01:00
Eleftheria Stein
95b39a203f Upgrade Spring Data to Neumann-M2
Resolves: #1567
2020-01-29 21:12:06 +01:00
Eleftheria Stein
3d653b3b50 Next Development Build 2020-01-29 21:08:14 +01:00
287 changed files with 14402 additions and 2430 deletions

View File

@@ -1,7 +1,17 @@
<!--
For Security Vulnerabilities, please use https://pivotal.io/security#reporting
!!! For Security Vulnerabilities, please go to https://spring.io/security-policy !!!
-->
**Affects:** \<Spring Framework version>
---
<!--
Thanks for raising a Spring Session issue. Please provide a brief description of your problem along with the version of Spring Session that you are using. If possible, please also consider putting together a sample application that reproduces the issue.
Thanks for taking the time to create an issue. Please read the following:
- Questions should be asked on Stack Overflow.
- For bugs, specify affected versions and explain what you are trying to do.
- For enhancements, provide context and describe the problem.
Issue or Pull Request? Create only one, not both. GitHub treats them as the same.
If unsure, start with an issue, and if you submit a pull request later, the
issue will be closed as superseded.
-->

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'type: bug, status: waiting-for-triage'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Sample**
A link to a GitHub repository with a [minimal, reproducible sample](https://stackoverflow.com/help/minimal-reproducible-example).
Reports that include a sample will take priority over reports that do not.
At times, we may require a sample, so it is good to try and include a sample up front.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Support
url: https://stackoverflow.com/questions/tagged/spring-session
about: Please ask and answer questions on StackOverflow with the tag spring-session

View File

@@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'status: waiting-for-triage, type: enhancement'
assignees: ''
---
**Expected Behavior**
<!--- Tell us how it should work -->
**Current Behavior**
<!--- Explain the difference from current behavior -->
**Context**
<!---
How has this issue affected you?
What are you trying to accomplish?
What other alternatives have you considered?
Are you aware of any workarounds?
-->

5
.github/actions/dispatch.sh vendored Executable file
View File

@@ -0,0 +1,5 @@
REPOSITORY_REF="$1"
TOKEN="$2"
curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches
echo "Requested Build for $REPOSITORY_REF"

27
.github/workflows/build-reference.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: reference
on:
push:
branches-ignore:
- 'gh-pages'
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Generate antora.yml
run: ./gradlew :spring-session-docs:generateAntora
- name: Push generated antora files to the spring-security-docs-generated
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: "spring-session/main" # The branch the action should deploy to.
folder: "spring-session-docs/build/generateAntora" # The folder the action should deploy.
repository-name: "spring-io/spring-generated-docs"
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Dispatch Build Request
run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'rwinch/spring-reference' "$GH_ACTIONS_REPO_TOKEN"

View File

@@ -0,0 +1,101 @@
name: CI
on:
push:
branches:
- main
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: github.repository == 'spring-projects/spring-session'
strategy:
matrix:
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Set up JDK ${{ matrix.jdk }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.jdk }}
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew clean build -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --no-daemon --stacktrace
artifacts:
name: Deploy Artifacts
needs: [build]
runs-on: ubuntu-latest
if: github.repository == 'spring-projects/spring-session'
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
- name: Deploy artifacts
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew deployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace --no-parallel
./gradlew finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace --no-parallel
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY_NO_HEADER }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_TOKEN_USERNAME }}
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_TOKEN_PASSWORD }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
docs:
name: Deploy Docs
needs: [build]
runs-on: ubuntu-latest
if: github.repository == 'spring-projects/spring-session'
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
- name: Deploy Docs
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew deployDocs --no-daemon -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
env:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}

View File

@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

26
.github/workflows/pr-build-workflow.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: PR Build
on: pull_request
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: github.repository == 'spring-projects/spring-session'
strategy:
matrix:
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Set up JDK ${{ matrix.jdk }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.jdk }}
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: ./gradlew clean build --no-daemon --stacktrace

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ out
.checkstyle
!etc/eclipse/.checkstyle
!**/src/**/build
.DS_Store

View File

@@ -1,16 +0,0 @@
sudo: required
language: java
jdk:
- openjdk8
- openjdk11
services:
- docker
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
install: true
script: ./gradlew clean check --no-daemon --stacktrace

View File

@@ -1,44 +0,0 @@
= Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open
and welcoming community, we pledge to respect all people who contribute through reporting
issues, posting feature requests, updating documentation, submitting pull requests or
patches, and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses,
without explicit permission
* Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this
Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors
that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
consistently applying these principles to every aspect of managing this project. Project
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
from the project team.
This Code of Conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
be reviewed and investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
with regard to the reporter of an incident.
This Code of Conduct is adapted from the
https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at
https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]

View File

@@ -3,9 +3,16 @@
Spring Session is released under the Apache 2.0 license. If you would like to contribute
something, or simply want to hack on the code this document should help you get started.
== Code of Conduct
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct].
== Reporting Security Vulnerabilities
Please see our https://github.com/spring-projects/spring-session/security/policy[Security policy].
== Using GitHub issues
@@ -19,7 +26,6 @@ information as possible. Ideally, that would include a small sample project that
reproduces the problem.
== Sign the Contributor License Agreement
If you have not previously done so, please fill out and
submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement].

181
Jenkinsfile vendored
View File

@@ -1,181 +0,0 @@
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
pipelineTriggers([
cron('@daily')
]),
])
def SUCCESS = hudson.model.Result.SUCCESS.toString()
currentBuild.result = SUCCESS
try {
parallel check: {
stage('Check') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
label 'spring-session'
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew clean check --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: check'
throw e
}
finally {
junit '**/build/test-results/*/*.xml'
}
}
}
}
},
jdk9: {
stage('JDK 9') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk9'}"]) {
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk9'
throw e
}
}
}
}
},
jdk10: {
stage('JDK 10') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk10'}"]) {
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk10'
throw e
}
}
}
}
},
jdk11: {
stage('JDK 11') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk11'}"]) {
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk11'
throw e
}
}
}
}
},
jdk12: {
stage('JDK 12') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
try {
withEnv(["JAVA_HOME=${tool 'openjdk12'}"]) {
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk12'
throw e
}
}
}
}
}
if (currentBuild.result == 'SUCCESS') {
parallel artifacts: {
stage('Deploy Artifacts') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withCredentials([file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')]) {
withCredentials([string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')]) {
withCredentials([usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')]) {
withCredentials([usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployArtifacts --no-daemon --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
sh './gradlew finalizeDeployArtifacts --no-daemon --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
}
}
}
}
}
}
catch (e) {
currentBuild.result = 'FAILED: artifacts'
throw e
}
}
}
},
docs: {
stage('Deploy Docs') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployDocs --no-daemon --stacktrace -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME'
}
}
}
catch (e) {
currentBuild.result = 'FAILED: docs'
throw e
}
}
}
}
}
}
finally {
def buildStatus = currentBuild.result
def buildNotSuccess = !SUCCESS.equals(buildStatus)
def lastBuildNotSuccess = !SUCCESS.equals(currentBuild.previousBuild?.result)
if (buildNotSuccess || lastBuildNotSuccess) {
stage('Notify') {
node {
final def RECIPIENTS = [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']]
def subject = "${buildStatus}: Build ${env.JOB_NAME} ${env.BUILD_NUMBER} status is now ${buildStatus}"
def details = "The build status changed to ${buildStatus}. For details see ${env.BUILD_URL}"
emailext(
subject: subject,
body: details,
recipientProviders: RECIPIENTS,
to: "$SPRING_SESSION_TEAM_EMAILS"
)
}
}
}
}

View File

@@ -1,6 +1,8 @@
= Spring Session
image:https://travis-ci.org/spring-projects/spring-session.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-session"] image:https://badges.gitter.im/spring-projects/spring-session.svg[link="https://gitter.im/spring-projects/spring-session?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"]
image:https://badges.gitter.im/spring-projects/spring-session.svg[link="https://gitter.im/spring-projects/spring-session?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"]
image:https://github.com/spring-projects/spring-session/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-session/actions?query=workflow%3ACI"]
Spring Session provides an API and implementations for managing a user's session information, while also making it trivial to support clustered sessions without being tied to an application container specific solution.
It also provides transparent integration with:
@@ -11,21 +13,61 @@ It also provides transparent integration with:
== Modules
Spring Session consists of the following modules:
This Spring Session repository consists of the following modules:
* Spring Session Core - provides core Spring Session functionalities and APIs
* Spring Session Data Redis - provides `SessionRepository` and `ReactiveSessionRepository` implementation backed by Redis and configuration support
* Spring Session JDBC - provides `SessionRepository` implementation backed by a relational database and configuration support
* Spring Session Hazelcast - provides `SessionRepository` implementation backed by Hazelcast and configuration support
Additional Spring Session modules can be found in the https://github.com/spring-projects/spring-session-data-mongodb[spring-session-data-mongodb] repository
and https://github.com/spring-projects/spring-session-data-geode[spring-session-data-geode] repository.
== Getting Started
We recommend you visit the https://docs.spring.io/spring-session/docs/current/reference/html5/#samples[Spring Session Reference] and look through the "Samples and Guides" section to see which one best suits your needs.
== Samples
Spring Session samples are available in the https://github.com/spring-projects/spring-session/tree/main/spring-session-samples[spring-session-samples] directory.
== Contributing
Please see our https://github.com/spring-projects/spring-session/blob/main/CONTRIBUTING.adoc[Contributing guidelines]
for information on how to report issues, enhancements or security vulnerabilities.
== Building from Source
Spring Session uses a https://gradle.org[Gradle]-based build system.
In the instructions below, `./gradlew` is invoked from the root of the source tree and serves as
a cross-platform, self-contained bootstrap mechanism for the build.
Check out sources
----
git clone git@github.com:spring-projects/spring-session.git
----
Install all spring-\* jars into your local Maven cache
----
./gradlew install
----
Compile and test; build all jars, distribution zips, and docs
----
./gradlew build
----
== Documentation
You can find the documentation, samples, and guides for using Spring Session on the https://projects.spring.io/spring-session/[Spring Session project site].
For more in depth information, visit the https://docs.spring.io/spring-session/docs/current/reference/html5/[Spring Session Reference].
== Code of Conduct
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
== Spring Session Project Site
You can find the documentation, issue management, support, samples, and guides for using Spring Session at https://projects.spring.io/spring-session/
Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct].
== License

View File

@@ -4,25 +4,46 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.2.7.RELEASE'
springBootVersion = '2.5.5'
}
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release/' }
maven {
url = 'https://repo.spring.io/plugins-snapshot'
if (project.hasProperty('artifactoryUsername')) {
credentials {
username "$artifactoryUsername"
password "$artifactoryPassword"
}
}
}
}
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.27.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.37'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
plugins {
id "io.github.rwinch.antora" version "0.0.2"
}
apply plugin: 'io.spring.convention.root'
group = 'org.springframework.session'
description = 'Spring Session'
antora {
playbookFile = file("local-antora-playbook.yml")
// default no version (current version)
antoraVersion = "3.0.0-alpha.9"
arguments = ["--fetch"]
}
subprojects {
apply plugin: 'io.spring.javaformat'

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
version=2.2.3.RELEASE
version=2.6.0-RC2

View File

@@ -1,35 +1,50 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-SR7'
mavenBom 'org.junit:junit-bom:5.5.2'
mavenBom 'org.springframework:spring-framework-bom:5.2.6.RELEASE'
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-SR7'
mavenBom 'org.springframework.security:spring-security-bom:5.2.4.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.12.5'
mavenBom 'io.projectreactor:reactor-bom:2020.0.12'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.11.2'
mavenBom 'org.junit:junit-bom:5.8.1'
mavenBom 'org.springframework:spring-framework-bom:5.3.11'
mavenBom 'org.springframework.data:spring-data-bom:2021.1.0-RC1'
mavenBom 'org.springframework.security:spring-security-bom:5.6.0-RC1'
mavenBom 'org.testcontainers:testcontainers-bom:1.16.0'
}
dependencies {
dependencySet(group: 'com.hazelcast', version: '3.12.5') {
dependencySet(group: 'com.hazelcast', version: '3.12.12') {
entry 'hazelcast'
entry 'hazelcast-client'
}
dependency 'com.h2database:h2:1.4.199'
dependency 'com.ibm.db2:jcc:11.5.0.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.4.1.jre8'
dependency 'com.oracle.ojdbc:ojdbc8:19.3.0.0'
dependency 'com.zaxxer:HikariCP:3.4.1'
dependency 'org.aspectj:aspectjweaver:1.9.7'
dependency 'ch.qos.logback:logback-core:1.2.3'
dependency 'com.google.code.findbugs:jsr305:3.0.2'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.6.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:9.4.0.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:21.3.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.2.2.RELEASE'
dependency 'io.lettuce:lettuce-core:6.1.5.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:4.0.1'
dependency 'junit:junit:4.12'
dependency 'mysql:mysql-connector-java:8.0.19'
dependency 'junit:junit:4.13.2'
dependency 'mysql:mysql-connector-java:8.0.26'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.13.2'
dependency 'org.hsqldb:hsqldb:2.5.0'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.4'
dependency 'org.mockito:mockito-core:3.0.0'
dependency 'org.postgresql:postgresql:42.2.12'
dependency 'org.assertj:assertj-core:3.21.0'
dependency 'org.hamcrest:hamcrest:2.1'
dependency 'org.hsqldb:hsqldb:2.5.2'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.4'
dependencySet(group: 'org.mockito', version: '4.0.0') {
entry 'mockito-core'
entry 'mockito-junit-jupiter'
}
dependencySet(group: 'org.mongodb', version: '4.2.3') {
entry 'mongodb-driver-core'
entry 'mongodb-driver-sync'
entry 'mongodb-driver-reactivestreams'
}
dependency 'org.postgresql:postgresql:42.2.24'
}
}

Binary file not shown.

View File

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

31
gradlew vendored
View File

@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -175,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

25
gradlew.bat vendored
View File

@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -51,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -61,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

14
local-antora-playbook.yml Normal file
View File

@@ -0,0 +1,14 @@
site:
title: Spring Session
start_page: session::index.adoc
content:
sources:
- url: https://github.com/spring-io/spring-generated-docs
branches: [spring-session/main]
- url: ./
branches: HEAD
start_path: spring-session-docs
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true

View File

@@ -1,10 +1,25 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release' }
}
}
plugins {
id "com.gradle.enterprise" version "3.5.1"
id "io.spring.ge.conventions" version "0.0.7"
}
rootProject.name = 'spring-session-build'
include 'spring-session-core'
include 'spring-session-data-mongodb'
include 'spring-session-data-redis'
include 'spring-session-docs'
include 'spring-session-hazelcast'
include 'spring-session-jdbc'
include 'hazelcast4'
project(':hazelcast4').projectDir = file('spring-session-hazelcast/hazelcast4')
file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
include dir.name

View File

@@ -25,5 +25,6 @@ dependencies {
testCompile "org.springframework.security:spring-security-core"
testCompile "org.junit.jupiter:junit-jupiter-api"
testCompile "org.junit.jupiter:junit-jupiter-params"
testCompile "org.aspectj:aspectjweaver"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
}

View File

@@ -40,7 +40,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
*
* {@literal @Bean}
* public MapSessionRepository sessionRepository() {
* return new MapSessionRepository();
* return new MapSessionRepository(new ConcurrentHashMap<>());
* }
*
* }

View File

@@ -58,7 +58,7 @@ import org.springframework.util.ObjectUtils;
*
* {@literal @Bean}
* public MapSessionRepository sessionRepository() {
* return new MapSessionRepository();
* return new MapSessionRepository(new ConcurrentHashMap<>());
* }
*
* }

View File

@@ -36,7 +36,7 @@ import org.springframework.context.annotation.Import;
*
* {@literal @Bean}
* public ReactiveSessionRepository sessionRepository() {
* return new ReactiveMapSessionRepository();
* return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
* }
*
* }

View File

@@ -38,12 +38,13 @@ import org.springframework.web.server.session.WebSessionManager;
@Configuration(proxyBeanMethods = false)
public class SpringWebSessionConfiguration {
/**
* Optional override of default {@link WebSessionIdResolver}.
*/
@Autowired(required = false)
private WebSessionIdResolver webSessionIdResolver;
@Autowired(required = false)
public void setWebSessionIdResolver(WebSessionIdResolver webSessionIdResolver) {
this.webSessionIdResolver = webSessionIdResolver;
}
/**
* Configure a {@link WebSessionManager} using a provided
* {@link ReactiveSessionRepository}.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -308,7 +308,7 @@ public class DefaultCookieSerializer implements CookieSerializer {
/**
* Sets the maxAge property of the Cookie. The default is to delete the cookie when
* the browser is closed.
* @param cookieMaxAge the maxAge property of the Cookie
* @param cookieMaxAge the maxAge property of the Cookie (defined in seconds)
*/
public void setCookieMaxAge(int cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
@@ -432,7 +432,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
private String getCookiePath(HttpServletRequest request) {
if (this.cookiePath == null) {
return request.getContextPath() + "/";
String contextPath = request.getContextPath();
return (contextPath != null && contextPath.length() > 0) ? contextPath : "/";
}
return this.cookiePath;
}

View File

@@ -64,7 +64,7 @@ abstract class OncePerRequestFilter implements Filter {
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName;
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {

View File

@@ -59,6 +59,7 @@ import org.springframework.session.SessionRepository;
* . The default is to look in a cookie named SESSION.</li>
* <li>The session id of newly created {@link org.springframework.session.Session} is sent
* to the client using
* {@link HttpSessionIdResolver#setSessionId(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, String)}
* <li>The client is notified that the session id is no longer valid with
* {@link HttpSessionIdResolver#expireSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
* </li>
@@ -309,6 +310,10 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
if (!create) {
return null;
}
if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
&& this.response.isCommitted()) {
throw new IllegalStateException("Cannot create a session after the response has been committed");
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "

View File

@@ -33,7 +33,7 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link SpringSessionRememberMeServices}.
@@ -88,7 +88,7 @@ class SpringSessionRememberMeServicesTests {
HttpServletResponse response = mock(HttpServletResponse.class);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.autoLogin(request, response);
verifyZeroInteractions(request, response);
verifyNoMoreInteractions(request, response);
}
// gh-752
@@ -102,7 +102,7 @@ class SpringSessionRememberMeServicesTests {
this.rememberMeServices.loginFail(request, response);
verify(request, times(1)).getSession(eq(false));
verify(session, times(1)).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
verifyZeroInteractions(request, response, session);
verifyNoMoreInteractions(request, response, session);
}
@Test
@@ -119,7 +119,7 @@ class SpringSessionRememberMeServicesTests {
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
verifyNoMoreInteractions(request, response, session, authentication);
}
@Test
@@ -137,7 +137,7 @@ class SpringSessionRememberMeServicesTests {
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
verifyNoMoreInteractions(request, response, session, authentication);
}
@Test
@@ -153,7 +153,7 @@ class SpringSessionRememberMeServicesTests {
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
verifyNoMoreInteractions(request, response, session, authentication);
}
@Test
@@ -171,7 +171,7 @@ class SpringSessionRememberMeServicesTests {
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(100000));
verifyZeroInteractions(request, response, session, authentication);
verifyNoMoreInteractions(request, response, session, authentication);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -109,7 +109,7 @@ class CookieHttpSessionIdResolverTests {
this.strategy.setSessionId(this.request, this.response, this.session.getId());
Cookie sessionCookie = this.response.getCookie(this.cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(this.request.getContextPath() + "/");
assertThat(sessionCookie.getPath()).isEqualTo(this.request.getContextPath());
}
@Test
@@ -131,7 +131,7 @@ class CookieHttpSessionIdResolverTests {
this.strategy.expireSession(this.request, this.response);
Cookie sessionCookie = this.response.getCookie(this.cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(this.request.getContextPath() + "/");
assertThat(sessionCookie.getPath()).isEqualTo(this.request.getContextPath());
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -267,7 +267,7 @@ class DefaultCookieSerializerTests {
void writeCookieCookiePathDefaultContextPathUsed() {
this.request.setContextPath("/context");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
assertThat(getCookie().getPath()).isEqualTo("/context");
}
@Test
@@ -275,7 +275,7 @@ class DefaultCookieSerializerTests {
this.request.setContextPath("/context");
this.serializer.setCookiePath(null);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
assertThat(getCookie().getPath()).isEqualTo("/context");
}
@Test

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2014-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.web.http;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.OncePerRequestFilterAopTests.Config;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThatCode;
@SpringJUnitConfig(classes = Config.class)
class OncePerRequestFilterAopTests {
@Test
void doFilterOnce(@Autowired final OncePerRequestFilter filter) {
assertThatCode(() -> filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockFilterChain())).as("`doFilter` does not throw NPE with the bean is being proxied by Spring AOP")
.doesNotThrowAnyException();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Aspect
public static class Config {
@Bean
public SessionRepository sessionRepository() {
return Mockito.mock(SessionRepository.class);
}
@Bean
public SessionRepositoryFilter filter() {
return new SessionRepositoryFilter(sessionRepository());
}
@AfterReturning("execution(* SessionRepositoryFilter.doFilterInternal(..))")
public void doInternalFilterPointcut() {
// no op
}
}
}

View File

@@ -39,7 +39,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link SessionEventHttpSessionListenerAdapter}.
@@ -97,7 +97,7 @@ class SessionEventHttpSessionListenerAdapterTests {
this.listener.onApplicationEvent(this.destroyed);
verifyZeroInteractions(this.destroyed, this.listener1, this.listener2);
verifyNoMoreInteractions(this.destroyed, this.listener1, this.listener2);
}
@Test

View File

@@ -62,6 +62,7 @@ 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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -72,7 +73,7 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link SessionRepositoryFilter}.
@@ -423,6 +424,18 @@ class SessionRepositoryFilterTests {
assertThat(this.response.getCookie("SESSION")).isNotNull();
}
@Test
void doFilterGetSessionNewWhenResponseCommittedThenException() {
assertThatIllegalStateException().isThrownBy(() -> doFilter(new DoInFilter() {
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse)
throws IOException {
wrappedResponse.getWriter().flush();
wrappedRequest.getSession();
}
}));
}
@Test
void doFilterGetSessionNew() throws Exception {
doFilter(new DoInFilter() {
@@ -1260,7 +1273,7 @@ class SessionRepositoryFilterTests {
}
});
verifyZeroInteractions(sessionRepository);
verifyNoMoreInteractions(sessionRepository);
}
@Test
@@ -1275,7 +1288,7 @@ class SessionRepositoryFilterTests {
}
});
verifyZeroInteractions(sessionRepository);
verifyNoMoreInteractions(sessionRepository);
}
@Test
@@ -1293,7 +1306,7 @@ class SessionRepositoryFilterTests {
}
});
verifyZeroInteractions(sessionRepository);
verifyNoMoreInteractions(sessionRepository);
}
@Test
@@ -1318,7 +1331,7 @@ class SessionRepositoryFilterTests {
verify(sessionRepository).deleteById(eq(session.getId()));
verify(sessionRepository).createSession();
verify(sessionRepository).save(any());
verifyZeroInteractions(sessionRepository);
verifyNoMoreInteractions(sessionRepository);
}
// --- order

View File

@@ -49,7 +49,7 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class SessionRepositoryMessageInterceptorTests {
@@ -98,7 +98,7 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -107,7 +107,7 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -116,7 +116,7 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -125,7 +125,7 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -209,7 +209,7 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -218,14 +218,14 @@ class SessionRepositoryMessageInterceptorTests {
assertThat(this.interceptor.preSend(createMessage(), this.channel)).isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
void beforeHandshakeNotServletServerHttpRequest() {
assertThat(this.interceptor.beforeHandshake(null, null, null, null)).isTrue();
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -233,7 +233,7 @@ class SessionRepositoryMessageInterceptorTests {
ServletServerHttpRequest request = new ServletServerHttpRequest(new MockHttpServletRequest());
assertThat(this.interceptor.beforeHandshake(request, null, null, null)).isTrue();
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
@Test
@@ -256,7 +256,7 @@ class SessionRepositoryMessageInterceptorTests {
void afterHandshakeDoesNothing() {
this.interceptor.afterHandshake(null, null, null, null);
verifyZeroInteractions(this.sessionRepository);
verifyNoMoreInteractions(this.sessionRepository);
}
private void setSessionId(String id) {

View File

@@ -0,0 +1,45 @@
apply plugin: 'io.spring.convention.spring-module'
description = "Spring Session and Spring MongoDB integration"
dependencies {
compile project(':spring-session-core')
// Spring Data MongoDB
compile("org.springframework.data:spring-data-mongodb") {
exclude group: "org.mongodb", module: "mongo-java-driver"
exclude group: "org.slf4j", module: "jcl-over-slf4j"
}
// MongoDB dependencies
optional "org.mongodb:mongodb-driver-core"
testCompile "org.mongodb:mongodb-driver-sync"
testCompile "org.mongodb:mongodb-driver-reactivestreams"
integrationTestCompile "org.testcontainers:mongodb"
// Everything else
compile "com.fasterxml.jackson.core:jackson-databind"
compile "org.springframework.security:spring-security-core"
compile "com.google.code.findbugs:jsr305"
optional "io.projectreactor:reactor-core"
testCompile "org.springframework:spring-web"
testCompile "org.springframework:spring-webflux"
testCompile "org.springframework.security:spring-security-config"
testCompile "org.springframework.security:spring-security-web"
testCompile "org.assertj:assertj-core"
testCompile "org.junit.jupiter:junit-jupiter-engine"
testCompile "org.junit.jupiter:junit-jupiter-params"
testCompile "org.springframework:spring-test"
testCompile "org.hamcrest:hamcrest"
testCompile "ch.qos.logback:logback-core"
testCompile "org.mockito:mockito-core"
testCompile "org.mockito:mockito-junit-jupiter"
testCompile "io.projectreactor:reactor-test"
testCompile "javax.servlet:javax.servlet-api"
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2018 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.mongo.integration;
import java.lang.reflect.Field;
import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.serializer.DefaultDeserializer;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.Assert;
import org.springframework.session.data.mongo.JdkMongoSessionConverter;
import org.springframework.util.ReflectionUtils;
/**
* Verify container's {@link ClassLoader} is injected into session converter (reactive and
* traditional).
*
* @author Greg Turnquist
*/
public abstract class AbstractClassLoaderTest<T> extends AbstractITest {
@Autowired
T sessionRepository;
@Autowired
ApplicationContext applicationContext;
@Test
void verifyContainerClassLoaderLoadedIntoConverter() {
Field mongoSessionConverterField = ReflectionUtils.findField(this.sessionRepository.getClass(),
"mongoSessionConverter");
ReflectionUtils.makeAccessible(
Assert.requireNonNull(mongoSessionConverterField, "mongoSessionConverter must not be null!"));
AbstractMongoSessionConverter sessionConverter = (AbstractMongoSessionConverter) ReflectionUtils
.getField(mongoSessionConverterField, this.sessionRepository);
AssertionsForClassTypes.assertThat(sessionConverter).isInstanceOf(JdkMongoSessionConverter.class);
JdkMongoSessionConverter jdkMongoSessionConverter = (JdkMongoSessionConverter) sessionConverter;
DeserializingConverter deserializingConverter = (DeserializingConverter) extractField(
JdkMongoSessionConverter.class, "deserializer", jdkMongoSessionConverter);
DefaultDeserializer deserializer = (DefaultDeserializer) extractField(DeserializingConverter.class,
"deserializer", deserializingConverter);
ClassLoader classLoader = (ClassLoader) extractField(DefaultDeserializer.class, "classLoader", deserializer);
AssertionsForClassTypes.assertThat(classLoader).isEqualTo(this.applicationContext.getClassLoader());
}
private static Object extractField(Class<?> clazz, String fieldName, Object obj) {
Field field = ReflectionUtils.findField(clazz, fieldName);
ReflectionUtils.makeAccessible(Assert.requireNonNull(field, fieldName + " must not be null!"));
return ReflectionUtils.getField(field, obj);
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.data.mongo.integration;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Base class for repositories integration tests
*
* @author Jakub Kubrynski
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
public abstract class AbstractITest {
protected SecurityContext context;
protected SecurityContext changedContext;
// @Autowired(required = false)
// protected SessionEventRegistry registry;
@BeforeEach
void setup() {
// if (this.registry != null) {
// this.registry.clear();
// }
this.context = SecurityContextHolder.createEmptyContext();
this.context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na",
AuthorityUtils.createAuthorityList("ROLE_USER")));
this.changedContext = SecurityContextHolder.createEmptyContext();
this.changedContext.setAuthentication(new UsernamePasswordAuthenticationToken(
"changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
}
}

View File

@@ -0,0 +1,408 @@
/*
* 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.data.mongo.integration;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
import org.springframework.session.data.mongo.MongoSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for {@link MongoIndexedSessionRepository} tests.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
public abstract class AbstractMongoRepositoryITest extends AbstractITest {
protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
protected static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
protected MongoIndexedSessionRepository repository;
@Test
void saves() {
String username = "saves-" + System.currentTimeMillis();
MongoSession toSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, toSaveContext);
toSave.setAttribute(INDEX_NAME, username);
this.repository.save(toSave);
Session session = this.repository.findById(toSave.getId());
assertThat(session.getId()).isEqualTo(toSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(toSave.getAttribute(expectedAttributeName));
this.repository.deleteById(toSave.getId());
String id = toSave.getId();
assertThat(this.repository.findById(id)).isNull();
}
@Test
void putAllOnSingleAttrDoesNotRemoveOld() {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute("a", "b");
this.repository.save(toSave);
toSave = this.repository.findById(toSave.getId());
toSave.setAttribute("1", "2");
this.repository.save(toSave);
toSave = this.repository.findById(toSave.getId());
Session session = this.repository.findById(toSave.getId());
assertThat(session.getAttributeNames().size()).isEqualTo(2);
assertThat(session.<String>getAttribute("a")).isEqualTo("b");
assertThat(session.<String>getAttribute("1")).isEqualTo("2");
this.repository.deleteById(toSave.getId());
}
@Test
void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
this.repository.deleteById(toSave.getId());
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
void nonExistentSessionShouldNotBreakMongo() {
this.repository.deleteById("doesn't exist");
}
@Test
void findByPrincipalNameNoPrincipalNameChange() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
void findByPrincipalNameNoPrincipalNameChangeReload() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave = this.repository.findById(toSave.getId());
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
void findByDeletedPrincipalName() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute(INDEX_NAME, null);
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByChangedPrincipalName() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(toSave);
Map<String, MongoSession> 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(toSave.getId());
}
@Test
void findByDeletedPrincipalNameReload() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
MongoSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByChangedPrincipalNameReload() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
MongoSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(getSession);
Map<String, MongoSession> 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(toSave.getId());
}
@Test
void findBySecurityPrincipalName() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
this.repository.deleteById(toSave.getId());
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
void findByPrincipalNameNoSecurityPrincipalNameChange() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
void findByDeletedSecurityPrincipalName() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, null);
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByChangedSecurityPrincipalName() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
void findByChangedSecurityPrincipalNameReload() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
MongoSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
this.repository.save(getSession);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
void loadExpiredSession() throws Exception {
// given
MongoSession expiredSession = this.repository.createSession();
Instant thirtyOneMinutesAgo = Instant.ofEpochMilli(System.currentTimeMillis()).minus(Duration.ofMinutes(31));
expiredSession.setLastAccessedTime(thirtyOneMinutesAgo);
this.repository.save(expiredSession);
// then
MongoSession expiredSessionFromDb = this.repository.findById(expiredSession.getId());
assertThat(expiredSessionFromDb).isNull();
}
protected String getSecurityName() {
return this.context.getAuthentication().getName();
}
protected String getChangedSecurityName() {
return this.changedContext.getAuthentication().getName();
}
protected static class BaseConfig {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@Bean(initMethod = "start", destroyMethod = "stop")
public MongoDBContainer mongoContainer() {
return new MongoDBContainer(DOCKER_IMAGE).withExposedPorts(27017);
}
@Bean
public MongoOperations mongoOperations(MongoDBContainer mongoContainer) {
MongoClient mongo = MongoClients.create(
"mongodb://" + mongoContainer.getContainerIpAddress() + ":" + mongoContainer.getFirstMappedPort());
return new MongoTemplate(mongo, "test");
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2019 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.mongo.integration;
import java.net.URI;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.MongoDBContainer;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JacksonMongoSessionConverter;
import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;
/**
* @author Boris Finkelshteyn
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class MongoDbDeleteJacksonSessionVerificationTest {
@Autowired
ApplicationContext ctx;
WebTestClient client;
@BeforeEach
void setUp() {
this.client = WebTestClient.bindToApplicationContext(this.ctx).build();
}
@Test
void logoutShouldDeleteOldSessionFromMongoDB() {
// 1. Login and capture the SESSION cookie value.
FluxExchangeResult<String> loginResult = this.client.post().uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED) //
.body(BodyInserters //
.fromFormData("username", "admin") //
.with("password", "password")) //
.exchange() //
.returnResult(String.class);
AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/"));
String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue();
// 2. Fetch a protected resource using the SESSION cookie.
this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isOk() //
.returnResult(String.class).getResponseBody() //
.as(StepVerifier::create) //
.expectNext("HelloWorld") //
.verifyComplete();
// 3. Logout using the SESSION cookie, and capture the new SESSION cookie.
String newSessionId = this.client.post().uri("/logout") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.returnResult(String.class).getResponseCookies().getFirst("SESSION").getValue();
AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId);
// 4. Verify the new SESSION cookie is not yet authorized.
this.client.get().uri("/hello") //
.cookie("SESSION", newSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader()
.value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login"));
// 5. Verify the original SESSION cookie no longer works.
this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader()
.value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login"));
}
@RestController
static class TestController {
@GetMapping("/hello")
ResponseEntity<String> hello() {
return ResponseEntity.ok("HelloWorld");
}
}
@EnableWebFluxSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http //
.logout()//
/**/.and() //
.formLogin() //
/**/.and() //
.csrf().disable() //
.authorizeExchange() //
.anyExchange().authenticated() //
/**/.and() //
.build();
}
@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(User.withDefaultPasswordEncoder() //
.username("admin") //
.password("password") //
.roles("USER,ADMIN") //
.build());
}
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return new JacksonMongoSessionConverter();
}
}
@Configuration
@EnableWebFlux
@EnableMongoWebSession
static class Config {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@Bean(initMethod = "start", destroyMethod = "stop")
MongoDBContainer mongoContainer() {
return new MongoDBContainer(DOCKER_IMAGE).withExposedPorts(27017);
}
@Bean
ReactiveMongoOperations mongoOperations(MongoDBContainer mongoContainer) {
MongoClient mongo = MongoClients.create(
"mongodb://" + mongoContainer.getContainerIpAddress() + ":" + mongoContainer.getFirstMappedPort());
return new ReactiveMongoTemplate(mongo, "DB_Name_DeleteJacksonSessionVerificationTest");
}
@Bean
TestController controller() {
return new TestController();
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2019 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.mongo.integration;
import java.net.URI;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.MongoDBContainer;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;
/**
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class MongoDbLogoutVerificationTest {
@Autowired
ApplicationContext ctx;
WebTestClient client;
@BeforeEach
void setUp() {
this.client = WebTestClient.bindToApplicationContext(this.ctx).build();
}
@Test
void logoutShouldDeleteOldSessionFromMongoDB() {
// 1. Login and capture the SESSION cookie value.
FluxExchangeResult<String> loginResult = this.client.post().uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED) //
.body(BodyInserters //
.fromFormData("username", "admin") //
.with("password", "password")) //
.exchange() //
.returnResult(String.class);
AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/"));
String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue();
// 2. Fetch a protected resource using the SESSION cookie.
this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isOk() //
.returnResult(String.class).getResponseBody() //
.as(StepVerifier::create) //
.expectNext("HelloWorld") //
.verifyComplete();
// 3. Logout using the SESSION cookie, and capture the new SESSION cookie.
String newSessionId = this.client.post().uri("/logout") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.returnResult(String.class).getResponseCookies().getFirst("SESSION").getValue();
AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId);
// 4. Verify the new SESSION cookie is not yet authorized.
this.client.get().uri("/hello") //
.cookie("SESSION", newSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader()
.value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login"));
// 5. Verify the original SESSION cookie no longer works.
this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader()
.value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login"));
}
@RestController
static class TestController {
@GetMapping("/hello")
ResponseEntity<String> hello() {
return ResponseEntity.ok("HelloWorld");
}
}
@EnableWebFluxSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http //
.logout()//
/**/.and() //
.formLogin() //
/**/.and() //
.csrf().disable() //
.authorizeExchange() //
.anyExchange().authenticated() //
/**/.and() //
.build();
}
@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(User.withDefaultPasswordEncoder() //
.username("admin") //
.password("password") //
.roles("USER,ADMIN") //
.build());
}
}
@Configuration
@EnableWebFlux
@EnableMongoWebSession
static class Config {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@Bean(initMethod = "start", destroyMethod = "stop")
MongoDBContainer mongoContainer() {
return new MongoDBContainer(DOCKER_IMAGE).withExposedPorts(27017);
}
@Bean
ReactiveMongoOperations mongoOperations(MongoDBContainer mongoContainer) {
MongoClient mongo = MongoClients.create(
"mongodb://" + mongoContainer.getContainerIpAddress() + ":" + mongoContainer.getFirstMappedPort());
return new ReactiveMongoTemplate(mongo, "test");
}
@Bean
TestController controller() {
return new TestController();
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.data.mongo.integration;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.geo.GeoModule;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JacksonMongoSessionConverter;
import org.springframework.session.data.mongo.MongoSession;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for
* {@link org.springframework.session.data.mongo.MongoIndexedSessionRepository} that use
* {@link JacksonMongoSessionConverter} based session serialization.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
@ContextConfiguration
public class MongoRepositoryJacksonITest extends AbstractMongoRepositoryITest {
@Test
void findByCustomIndex() throws Exception {
MongoSession toSave = this.repository.createSession();
String cartId = "cart-" + UUID.randomUUID();
toSave.setAttribute("cartId", cartId);
this.repository.save(toSave);
Map<String, MongoSession> findByCartId = this.repository.findByIndexNameAndIndexValue("cartId", cartId);
assertThat(findByCartId).hasSize(1);
assertThat(findByCartId.keySet()).containsOnly(toSave.getId());
}
// tag::sample[]
@Configuration
@EnableMongoHttpSession
static class Config extends BaseConfig {
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return new JacksonMongoSessionConverter(Collections.singletonList(new GeoModule()));
}
}
// end::sample[]
}

View File

@@ -0,0 +1,96 @@
/*
* 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.data.mongo.integration;
import java.time.Duration;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JdkMongoSessionConverter;
import org.springframework.session.data.mongo.MongoSession;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for
* {@link org.springframework.session.data.mongo.MongoIndexedSessionRepository} that use
* {@link JdkMongoSessionConverter} based session serialization.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
@ContextConfiguration
public class MongoRepositoryJdkSerializationITest extends AbstractMongoRepositoryITest {
@Test
void findByDeletedSecurityPrincipalNameReload() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
MongoSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getChangedSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByPrincipalNameNoSecurityPrincipalNameChangeReload() throws Exception {
MongoSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave = this.repository.findById(toSave.getId());
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, MongoSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
// tag::sample[]
@Configuration
@EnableMongoHttpSession
static class Config extends BaseConfig {
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return new JdkMongoSessionConverter(Duration.ofMinutes(30));
}
}
// end::sample[]
}

View File

@@ -0,0 +1,129 @@
/*
* 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.data.mongo;
import java.util.Collections;
import java.util.Set;
import com.mongodb.DBObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.IndexResolver;
import org.springframework.session.PrincipalNameIndexResolver;
/**
* Base class for serializing and deserializing session objects. To create custom
* serializer you have to implement this interface and simply register your class as a
* bean.
*
* @author Jakub Kubrynski
* @author Greg Turnquist
* @since 1.2
*/
public abstract class AbstractMongoSessionConverter implements GenericConverter {
static final String EXPIRE_AT_FIELD_NAME = "expireAt";
private static final Log LOG = LogFactory.getLog(AbstractMongoSessionConverter.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private IndexResolver<MongoSession> indexResolver = new DelegatingIndexResolver<>(
new PrincipalNameIndexResolver<>());
/**
* Returns query to be executed to return sessions based on a particular index.
* @param indexName name of the index
* @param indexValue value to query against
* @return built query or null if indexName is not supported
*/
@Nullable
protected abstract Query getQueryForIndex(String indexName, Object indexValue);
/**
* Method ensures that there is a TTL index on {@literal expireAt} field. It's has
* {@literal expireAfterSeconds} set to zero seconds, so the expiration time is
* controlled by the application. It can be extended in custom converters when there
* is a need for creating additional custom indexes.
* @param sessionCollectionIndexes {@link IndexOperations} to use
*/
protected void ensureIndexes(IndexOperations sessionCollectionIndexes) {
for (IndexInfo info : sessionCollectionIndexes.getIndexInfo()) {
if (EXPIRE_AT_FIELD_NAME.equals(info.getName())) {
LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists");
return;
}
}
LOG.info("Creating TTL index on field " + EXPIRE_AT_FIELD_NAME);
sessionCollectionIndexes
.ensureIndex(new Index(EXPIRE_AT_FIELD_NAME, Sort.Direction.ASC).named(EXPIRE_AT_FIELD_NAME).expire(0));
}
protected String extractPrincipal(MongoSession expiringSession) {
return this.indexResolver.resolveIndexesFor(expiringSession)
.get(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
}
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(DBObject.class, MongoSession.class));
}
@SuppressWarnings("unchecked")
@Nullable
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
if (DBObject.class.isAssignableFrom(sourceType.getType())) {
return convert(new Document(((DBObject) source).toMap()));
}
else if (Document.class.isAssignableFrom(sourceType.getType())) {
return convert((Document) source);
}
else {
return convert((MongoSession) source);
}
}
protected abstract DBObject convert(MongoSession session);
protected abstract MongoSession convert(Document sessionWrapper);
public void setIndexResolver(IndexResolver<MongoSession> indexResolver) {
this.indexResolver = Assert.requireNonNull(indexResolver, "indexResolver must not be null!");
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2019 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.mongo;
import org.springframework.lang.Nullable;
/**
* Utility to verify non null fields.
*
* @author Greg Turnquist
*/
public final class Assert {
private Assert() {
}
public static <T> T requireNonNull(@Nullable T item, String message) {
if (item == null) {
throw new IllegalArgumentException(message);
}
return item;
}
}

View File

@@ -0,0 +1,187 @@
/*
* 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.data.mongo;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriterSettings;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.util.Assert;
/**
* {@code AbstractMongoSessionConverter} implementation using Jackson.
*
* @author Jakub Kubrynski
* @author Greg Turnquist
* @author Michael Ruf
* @since 1.2
*/
public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter {
private static final Log LOG = LogFactory.getLog(JacksonMongoSessionConverter.class);
private static final String ATTRS_FIELD_NAME = "attrs.";
private static final String PRINCIPAL_FIELD_NAME = "principal";
private static final String EXPIRE_AT_FIELD_NAME = "expireAt";
private final ObjectMapper objectMapper;
public JacksonMongoSessionConverter() {
this(Collections.emptyList());
}
public JacksonMongoSessionConverter(Iterable<Module> modules) {
this.objectMapper = buildObjectMapper();
this.objectMapper.registerModules(modules);
}
public JacksonMongoSessionConverter(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper can NOT be null!");
this.objectMapper = objectMapper;
}
@Nullable
protected Query getQueryForIndex(String indexName, Object indexValue) {
if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue));
}
else {
return Query.query(Criteria.where(ATTRS_FIELD_NAME + MongoSession.coverDot(indexName)).is(indexValue));
}
}
private ObjectMapper buildObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// serialize fields instead of properties
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
// ignore unresolved fields (mostly 'principal')
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setPropertyNamingStrategy(new MongoIdNamingStrategy());
objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
objectMapper.addMixIn(MongoSession.class, MongoSessionMixin.class);
objectMapper.addMixIn(HashMap.class, HashMapMixin.class);
return objectMapper;
}
@Override
protected DBObject convert(MongoSession source) {
try {
DBObject dbSession = BasicDBObject.parse(this.objectMapper.writeValueAsString(source));
// Override default serialization with proper values.
dbSession.put(PRINCIPAL_FIELD_NAME, extractPrincipal(source));
dbSession.put(EXPIRE_AT_FIELD_NAME, source.getExpireAt());
return dbSession;
}
catch (JsonProcessingException ex) {
throw new IllegalStateException("Cannot convert MongoExpiringSession", ex);
}
}
@Override
@Nullable
protected MongoSession convert(Document source) {
Date expireAt = (Date) source.remove(EXPIRE_AT_FIELD_NAME);
source.remove("originalSessionId");
String json = source.toJson(JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build());
try {
MongoSession mongoSession = this.objectMapper.readValue(json, MongoSession.class);
mongoSession.setExpireAt(expireAt);
return mongoSession;
}
catch (IOException ex) {
LOG.error("Error during Mongo Session deserialization", ex);
return null;
}
}
/**
* Used to whitelist {@link MongoSession} for {@link SecurityJackson2Modules}.
*/
private static class MongoSessionMixin {
@JsonCreator
MongoSessionMixin(@JsonProperty("_id") String id,
@JsonProperty("intervalSeconds") long maxInactiveIntervalInSeconds) {
}
}
/**
* Used to whitelist {@link HashMap} for {@link SecurityJackson2Modules}.
*/
private static class HashMapMixin {
// Nothing special
}
private static class MongoIdNamingStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase {
@Override
public String translate(String propertyName) {
switch (propertyName) {
case "id":
return "_id";
case "_id":
return "id";
default:
return propertyName;
}
}
}
}

View File

@@ -0,0 +1,174 @@
/*
* 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.data.mongo;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.bson.Document;
import org.bson.types.Binary;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* {@code AbstractMongoSessionConverter} implementation using standard Java serialization.
*
* @author Jakub Kubrynski
* @author Rob Winch
* @author Greg Turnquist
* @since 1.2
*/
public class JdkMongoSessionConverter extends AbstractMongoSessionConverter {
private static final String ID = "_id";
private static final String CREATION_TIME = "created";
private static final String LAST_ACCESSED_TIME = "accessed";
private static final String MAX_INTERVAL = "interval";
private static final String ATTRIBUTES = "attr";
private static final String PRINCIPAL_FIELD_NAME = "principal";
private final Converter<Object, byte[]> serializer;
private final Converter<byte[], Object> deserializer;
private Duration maxInactiveInterval;
public JdkMongoSessionConverter(Duration maxInactiveInterval) {
this(new SerializingConverter(), new DeserializingConverter(), maxInactiveInterval);
}
public JdkMongoSessionConverter(Converter<Object, byte[]> serializer, Converter<byte[], Object> deserializer,
Duration maxInactiveInterval) {
Assert.notNull(serializer, "serializer cannot be null");
Assert.notNull(deserializer, "deserializer cannot be null");
Assert.notNull(maxInactiveInterval, "maxInactiveInterval cannot be null");
this.serializer = serializer;
this.deserializer = deserializer;
this.maxInactiveInterval = maxInactiveInterval;
}
@Override
@Nullable
public Query getQueryForIndex(String indexName, Object indexValue) {
if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue));
}
else {
return null;
}
}
@Override
protected DBObject convert(MongoSession session) {
BasicDBObject basicDBObject = new BasicDBObject();
basicDBObject.put(ID, session.getId());
basicDBObject.put(CREATION_TIME, session.getCreationTime());
basicDBObject.put(LAST_ACCESSED_TIME, session.getLastAccessedTime());
basicDBObject.put(MAX_INTERVAL, session.getMaxInactiveInterval());
basicDBObject.put(PRINCIPAL_FIELD_NAME, extractPrincipal(session));
basicDBObject.put(EXPIRE_AT_FIELD_NAME, session.getExpireAt());
basicDBObject.put(ATTRIBUTES, serializeAttributes(session));
return basicDBObject;
}
@Override
protected MongoSession convert(Document sessionWrapper) {
Object maxInterval = sessionWrapper.getOrDefault(MAX_INTERVAL, this.maxInactiveInterval);
Duration maxIntervalDuration = (maxInterval instanceof Duration) ? (Duration) maxInterval
: Duration.parse(maxInterval.toString());
MongoSession session = new MongoSession(sessionWrapper.getString(ID), maxIntervalDuration.getSeconds());
Object creationTime = sessionWrapper.get(CREATION_TIME);
if (creationTime instanceof Instant) {
session.setCreationTime(((Instant) creationTime).toEpochMilli());
}
else if (creationTime instanceof Date) {
session.setCreationTime(((Date) creationTime).getTime());
}
Object lastAccessedTime = sessionWrapper.get(LAST_ACCESSED_TIME);
if (lastAccessedTime instanceof Instant) {
session.setLastAccessedTime((Instant) lastAccessedTime);
}
else if (lastAccessedTime instanceof Date) {
session.setLastAccessedTime(Instant.ofEpochMilli(((Date) lastAccessedTime).getTime()));
}
session.setExpireAt((Date) sessionWrapper.get(EXPIRE_AT_FIELD_NAME));
deserializeAttributes(sessionWrapper, session);
return session;
}
@Nullable
private byte[] serializeAttributes(Session session) {
Map<String, Object> attributes = new HashMap<>();
for (String attrName : session.getAttributeNames()) {
attributes.put(attrName, session.getAttribute(attrName));
}
return this.serializer.convert(attributes);
}
@SuppressWarnings("unchecked")
private void deserializeAttributes(Document sessionWrapper, Session session) {
Object sessionAttributes = sessionWrapper.get(ATTRIBUTES);
byte[] attributesBytes = ((sessionAttributes instanceof Binary) ? ((Binary) sessionAttributes).getData()
: (byte[]) sessionAttributes);
Map<String, Object> attributes = (Map<String, Object>) this.deserializer.convert(attributesBytes);
if (attributes != null) {
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
session.setAttribute(entry.getKey(), entry.getValue());
}
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2019 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.mongo;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.lang.Nullable;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
/**
* Session repository implementation which stores sessions in Mongo. Uses
* {@link AbstractMongoSessionConverter} to transform session objects from/to native Mongo
* representation ({@code DBObject}). Repository is also responsible for removing expired
* sessions from database. Cleanup is done every minute.
*
* @author Jakub Kubrynski
* @author Greg Turnquist
* @since 2.2.0
*/
public class MongoIndexedSessionRepository
implements FindByIndexNameSessionRepository<MongoSession>, ApplicationEventPublisherAware, InitializingBean {
/**
* The default time period in seconds in which a session will expire.
*/
public static final int DEFAULT_INACTIVE_INTERVAL = 1800;
/**
* the default collection name for storing session.
*/
public static final String DEFAULT_COLLECTION_NAME = "sessions";
private static final Log logger = LogFactory.getLog(MongoIndexedSessionRepository.class);
private final MongoOperations mongoOperations;
private Integer maxInactiveIntervalInSeconds = DEFAULT_INACTIVE_INTERVAL;
private String collectionName = DEFAULT_COLLECTION_NAME;
private AbstractMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(
Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
private ApplicationEventPublisher eventPublisher;
public MongoIndexedSessionRepository(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
@Override
public MongoSession createSession() {
MongoSession session = new MongoSession();
if (this.maxInactiveIntervalInSeconds != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
}
publishEvent(new SessionCreatedEvent(this, session));
return session;
}
@Override
public void save(MongoSession session) {
this.mongoOperations
.save(Assert.requireNonNull(MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session),
"convertToDBObject must not null!"), this.collectionName);
}
@Override
@Nullable
public MongoSession findById(String id) {
Document sessionWrapper = findSession(id);
if (sessionWrapper == null) {
return null;
}
MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, sessionWrapper);
if (session != null && session.isExpired()) {
publishEvent(new SessionExpiredEvent(this, session));
deleteById(id);
return null;
}
return session;
}
/**
* Currently this repository allows only querying against
* {@code PRINCIPAL_NAME_INDEX_NAME}.
* @param indexName the name if the index (i.e.
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
* @param indexValue the value of the index to search for.
* @return sessions map
*/
@Override
public Map<String, MongoSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
return Optional.ofNullable(this.mongoSessionConverter.getQueryForIndex(indexName, indexValue))
.map((query) -> this.mongoOperations.find(query, Document.class, this.collectionName))
.orElse(Collections.emptyList()).stream()
.map((dbSession) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, dbSession))
.collect(Collectors.toMap(MongoSession::getId, (mapSession) -> mapSession));
}
@Override
public void deleteById(String id) {
Optional.ofNullable(findSession(id)).ifPresent((document) -> {
MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, document);
if (session != null) {
publishEvent(new SessionDeletedEvent(this, session));
}
this.mongoOperations.remove(document, this.collectionName);
});
}
@Override
public void afterPropertiesSet() {
IndexOperations indexOperations = this.mongoOperations.indexOps(this.collectionName);
this.mongoSessionConverter.ensureIndexes(indexOperations);
}
@Nullable
private Document findSession(String id) {
return this.mongoOperations.findById(id, Document.class, this.collectionName);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public void setMaxInactiveIntervalInSeconds(final Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setCollectionName(final String collectionName) {
this.collectionName = collectionName;
}
public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2014-2017 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.mongo;
import org.springframework.data.mongodb.core.MongoOperations;
/**
* This {@link org.springframework.session.FindByIndexNameSessionRepository}
* implementation is kept to support backwards compatibility.
*
* @author Rob Winch
* @since 1.2
* @deprecated since 2.2.0 in favor of {@link MongoIndexedSessionRepository}.
*/
@Deprecated
public class MongoOperationsSessionRepository extends MongoIndexedSessionRepository {
public MongoOperationsSessionRepository(MongoOperations mongoOperations) {
super(mongoOperations);
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.data.mongo;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.session.Session;
/**
* Session object providing additional information about the datetime of expiration.
*
* @author Jakub Kubrynski
* @author Greg Turnquist
* @since 1.2
*/
public class MongoSession implements Session {
/**
* Mongo doesn't support {@literal dot} in field names. We replace it with a very
* rarely used character
*/
private static final char DOT_COVER_CHAR = '';
private String id;
private String originalSessionId;
private long createdMillis = System.currentTimeMillis();
private long accessedMillis;
private long intervalSeconds;
private Date expireAt;
private Map<String, Object> attrs = new HashMap<>();
public MongoSession() {
this(MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL);
}
public MongoSession(long maxInactiveIntervalInSeconds) {
this(UUID.randomUUID().toString(), maxInactiveIntervalInSeconds);
}
public MongoSession(String id, long maxInactiveIntervalInSeconds) {
this.id = id;
this.originalSessionId = id;
this.intervalSeconds = maxInactiveIntervalInSeconds;
setLastAccessedTime(Instant.ofEpochMilli(this.createdMillis));
}
static String coverDot(String attributeName) {
return attributeName.replace('.', DOT_COVER_CHAR);
}
static String uncoverDot(String attributeName) {
return attributeName.replace(DOT_COVER_CHAR, '.');
}
@Override
public String changeSessionId() {
String changedId = UUID.randomUUID().toString();
this.id = changedId;
return changedId;
}
@Override
@Nullable
public <T> T getAttribute(String attributeName) {
return (T) this.attrs.get(coverDot(attributeName));
}
@Override
public Set<String> getAttributeNames() {
return this.attrs.keySet().stream().map(MongoSession::uncoverDot).collect(Collectors.toSet());
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
if (attributeValue == null) {
removeAttribute(coverDot(attributeName));
}
else {
this.attrs.put(coverDot(attributeName), attributeValue);
}
}
@Override
public void removeAttribute(String attributeName) {
this.attrs.remove(coverDot(attributeName));
}
@Override
public Instant getCreationTime() {
return Instant.ofEpochMilli(this.createdMillis);
}
public void setCreationTime(long created) {
this.createdMillis = created;
}
@Override
public Instant getLastAccessedTime() {
return Instant.ofEpochMilli(this.accessedMillis);
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.accessedMillis = lastAccessedTime.toEpochMilli();
this.expireAt = Date.from(lastAccessedTime.plus(Duration.ofSeconds(this.intervalSeconds)));
}
@Override
public Duration getMaxInactiveInterval() {
return Duration.ofSeconds(this.intervalSeconds);
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.intervalSeconds = interval.getSeconds();
}
@Override
public boolean isExpired() {
return this.intervalSeconds >= 0 && new Date().after(this.expireAt);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MongoSession that = (MongoSession) o;
return Objects.equals(this.id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(this.id);
}
@Override
public String getId() {
return this.id;
}
public Date getExpireAt() {
return this.expireAt;
}
public void setExpireAt(final Date expireAt) {
this.expireAt = expireAt;
}
boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
String getOriginalSessionId() {
return this.originalSessionId;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 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.mongo;
import com.mongodb.DBObject;
import org.bson.Document;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
/**
* Utility for MongoSession.
*
* @author Greg Turnquist
*/
public final class MongoSessionUtils {
private MongoSessionUtils() {
}
@Nullable
static DBObject convertToDBObject(AbstractMongoSessionConverter mongoSessionConverter, MongoSession session) {
return (DBObject) mongoSessionConverter.convert(session, TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class));
}
@Nullable
static MongoSession convertToSession(AbstractMongoSessionConverter mongoSessionConverter, Document session) {
return (MongoSession) mongoSessionConverter.convert(session, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class));
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2017 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.mongo;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.session.ReactiveSessionRepository;
/**
* This {@link ReactiveSessionRepository} implementation is kept to support migration to
* {@link ReactiveMongoSessionRepository} in a backwards compatible manner.
*
* @author Greg Turnquist
* @deprecated since 2.2.0 in favor of {@link ReactiveMongoSessionRepository}.
*/
@Deprecated
public class ReactiveMongoOperationsSessionRepository extends ReactiveMongoSessionRepository {
public ReactiveMongoOperationsSessionRepository(ReactiveMongoOperations mongoOperations) {
super(mongoOperations);
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2019 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.mongo;
import java.time.Duration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
/**
* A {@link ReactiveSessionRepository} implementation that uses Spring Data MongoDB.
*
* @author Greg Turnquist
* @since 2.2.0
*/
public class ReactiveMongoSessionRepository
implements ReactiveSessionRepository<MongoSession>, ApplicationEventPublisherAware, InitializingBean {
/**
* The default time period in seconds in which a session will expire.
*/
public static final int DEFAULT_INACTIVE_INTERVAL = 1800;
/**
* The default collection name for storing session.
*/
public static final String DEFAULT_COLLECTION_NAME = "sessions";
private static final Log logger = LogFactory.getLog(ReactiveMongoSessionRepository.class);
private final ReactiveMongoOperations mongoOperations;
private Integer maxInactiveIntervalInSeconds = DEFAULT_INACTIVE_INTERVAL;
private String collectionName = DEFAULT_COLLECTION_NAME;
private AbstractMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(
Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
private MongoOperations blockingMongoOperations;
private ApplicationEventPublisher eventPublisher;
public ReactiveMongoSessionRepository(ReactiveMongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
/**
* Creates a new {@link MongoSession} that is capable of being persisted by this
* {@link ReactiveSessionRepository}.
* <p>
* This allows optimizations and customizations in how the {@link MongoSession} is
* persisted. For example, the implementation returned might keep track of the changes
* ensuring that only the delta needs to be persisted on a save.
* </p>
* @return a new {@link MongoSession} that is capable of being persisted by this
* {@link ReactiveSessionRepository}
*/
@Override
public Mono<MongoSession> createSession() {
return Mono.justOrEmpty(this.maxInactiveIntervalInSeconds) //
.map(MongoSession::new) //
.doOnNext((mongoSession) -> publishEvent(new SessionCreatedEvent(this, mongoSession))) //
.switchIfEmpty(Mono.just(new MongoSession()));
}
@Override
public Mono<Void> save(MongoSession session) {
return Mono //
.justOrEmpty(MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session)) //
.flatMap((dbObject) -> {
if (session.hasChangedSessionId()) {
return this.mongoOperations
.remove(Query.query(Criteria.where("_id").is(session.getOriginalSessionId())),
this.collectionName) //
.then(this.mongoOperations.save(dbObject, this.collectionName));
}
else {
return this.mongoOperations.save(dbObject, this.collectionName);
}
}) //
.then();
}
@Override
public Mono<MongoSession> findById(String id) {
return findSession(id) //
.map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) //
.filter((mongoSession) -> !mongoSession.isExpired()) //
.switchIfEmpty(Mono.defer(() -> this.deleteById(id).then(Mono.empty())));
}
@Override
public Mono<Void> deleteById(String id) {
return findSession(id) //
.flatMap((document) -> this.mongoOperations.remove(document, this.collectionName) //
.then(Mono.just(document))) //
.map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) //
.doOnNext((mongoSession) -> publishEvent(new SessionDeletedEvent(this, mongoSession))) //
.then();
}
/**
* Do not use
* {@link org.springframework.data.mongodb.core.index.ReactiveIndexOperations} to
* ensure indexes exist. Instead, get a blocking {@link IndexOperations} and use that
* instead, if possible.
*/
@Override
public void afterPropertiesSet() {
if (this.blockingMongoOperations != null) {
IndexOperations indexOperations = this.blockingMongoOperations.indexOps(this.collectionName);
this.mongoSessionConverter.ensureIndexes(indexOperations);
}
}
private Mono<Document> findSession(String id) {
return this.mongoOperations.findById(id, Document.class, this.collectionName);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public Integer getMaxInactiveIntervalInSeconds() {
return this.maxInactiveIntervalInSeconds;
}
public void setMaxInactiveIntervalInSeconds(final Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public String getCollectionName() {
return this.collectionName;
}
public void setCollectionName(final String collectionName) {
this.collectionName = collectionName;
}
public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
public void setBlockingMongoOperations(final MongoOperations blockingMongoOperations) {
this.blockingMongoOperations = blockingMongoOperations;
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.data.mongo.config.annotation.web.http;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
/**
* Add this annotation to a {@code @Configuration} class to expose the
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and backed by
* Mongo. Use {@code collectionName} to change default name of the collection used to
* store sessions.
*
* <pre>
* <code>
* {@literal @EnableMongoHttpSession}
* public class MongoHttpSessionConfig {
*
* {@literal @Bean}
* public MongoOperations mongoOperations() throws UnknownHostException {
* return new MongoTemplate(new MongoClient(), "databaseName");
* }
*
* }
* </code> </pre>
*
* @author Jakub Kubrynski
* @since 1.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MongoHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableMongoHttpSession {
/**
* The maximum time a session will be kept if it is inactive.
* @return default max inactive interval in seconds
*/
int maxInactiveIntervalInSeconds() default MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL;
/**
* The collection name to use.
* @return name of the collection to store session
*/
String collectionName() default MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME;
}

View File

@@ -0,0 +1,157 @@
/*
* 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.data.mongo.config.annotation.web.http;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
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.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.session.IndexResolver;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JdkMongoSessionConverter;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
import org.springframework.session.data.mongo.MongoSession;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Configuration class registering {@code MongoSessionRepository} bean. To import this
* configuration use {@link EnableMongoHttpSession} annotation.
*
* @author Jakub Kubrynski
* @author Eddú Meléndez
* @since 1.2
*/
@Configuration(proxyBeanMethods = false)
public class MongoHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
private AbstractMongoSessionConverter mongoSessionConverter;
private Integer maxInactiveIntervalInSeconds;
private String collectionName;
private StringValueResolver embeddedValueResolver;
private List<SessionRepositoryCustomizer<MongoIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private IndexResolver<MongoSession> indexResolver;
@Bean
public MongoIndexedSessionRepository mongoSessionRepository(MongoOperations mongoOperations) {
MongoIndexedSessionRepository repository = new MongoIndexedSessionRepository(mongoOperations);
repository.setMaxInactiveIntervalInSeconds(this.maxInactiveIntervalInSeconds);
if (this.mongoSessionConverter != null) {
repository.setMongoSessionConverter(this.mongoSessionConverter);
if (this.indexResolver != null) {
this.mongoSessionConverter.setIndexResolver(this.indexResolver);
}
}
else {
JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(new SerializingConverter(),
new DeserializingConverter(this.classLoader),
Duration.ofSeconds(MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL));
if (this.indexResolver != null) {
mongoSessionConverter.setIndexResolver(this.indexResolver);
}
repository.setMongoSessionConverter(mongoSessionConverter);
}
if (StringUtils.hasText(this.collectionName)) {
repository.setCollectionName(this.collectionName);
}
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository));
return repository;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(importMetadata.getAnnotationAttributes(EnableMongoHttpSession.class.getName()));
if (attributes != null) {
this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
}
else {
this.maxInactiveIntervalInSeconds = MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL;
}
String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : "";
if (StringUtils.hasText(collectionNameValue)) {
this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue);
}
}
@Autowired(required = false)
public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizers(
ObjectProvider<SessionRepositoryCustomizer<MongoIndexedSessionRepository>> 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;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<MongoSession> indexResolver) {
this.indexResolver = indexResolver;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2017 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.mongo.config.annotation.web.reactive;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
/**
* Add this annotation to a {@code @Configuration} class to configure a MongoDB-based
* {@code WebSessionManager} for a WebFlux application. This annotation assumes a
* {@code ReactorMongoOperations} is defined somewhere in the application context. If not,
* it will fail with a clear error messages. For example:
*
* <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableMongoWebSession}
* public class SpringWebFluxConfig {
*
* {@literal @Bean}
* public ReactorMongoOperations operations() {
* return new MaReactorMongoOperations(...);
* }
*
* }
* </code> </pre>
*
* @author Greg Turnquist
* @author Vedran Pavić
* @since 2.0
*/
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(ReactiveMongoWebSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableMongoWebSession {
/**
* The maximum time a session will be kept if it is inactive.
* @return default max inactive interval in seconds
*/
int maxInactiveIntervalInSeconds() default ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL;
/**
* The collection name to use.
* @return name of the collection to store session
*/
String collectionName() default ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME;
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2017 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.mongo.config.annotation.web.reactive;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
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.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.session.IndexResolver;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JdkMongoSessionConverter;
import org.springframework.session.data.mongo.MongoSession;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Configure a {@link ReactiveMongoSessionRepository} using a provided
* {@link ReactiveMongoOperations}.
*
* @author Greg Turnquist
* @author Vedran Pavić
*/
@Configuration(proxyBeanMethods = false)
public class ReactiveMongoWebSessionConfiguration extends SpringWebSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
private AbstractMongoSessionConverter mongoSessionConverter;
private Integer maxInactiveIntervalInSeconds;
private String collectionName;
private StringValueResolver embeddedValueResolver;
private List<ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository>> sessionRepositoryCustomizers;
@Autowired(required = false)
private MongoOperations mongoOperations;
private ClassLoader classLoader;
private IndexResolver<MongoSession> indexResolver;
@Bean
public ReactiveMongoSessionRepository reactiveMongoSessionRepository(ReactiveMongoOperations operations) {
ReactiveMongoSessionRepository repository = new ReactiveMongoSessionRepository(operations);
if (this.mongoSessionConverter != null) {
repository.setMongoSessionConverter(this.mongoSessionConverter);
if (this.indexResolver != null) {
this.mongoSessionConverter.setIndexResolver(this.indexResolver);
}
}
else {
JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(new SerializingConverter(),
new DeserializingConverter(this.classLoader),
Duration.ofSeconds(ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL));
if (this.indexResolver != null) {
mongoSessionConverter.setIndexResolver(this.indexResolver);
}
repository.setMongoSessionConverter(mongoSessionConverter);
}
if (this.maxInactiveIntervalInSeconds != null) {
repository.setMaxInactiveIntervalInSeconds(this.maxInactiveIntervalInSeconds);
}
if (this.collectionName != null) {
repository.setCollectionName(this.collectionName);
}
if (this.mongoOperations != null) {
repository.setBlockingMongoOperations(this.mongoOperations);
}
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository));
return repository;
}
@Autowired(required = false)
public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(importMetadata.getAnnotationAttributes(EnableMongoWebSession.class.getName()));
if (attributes != null) {
this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
}
else {
this.maxInactiveIntervalInSeconds = ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL;
}
String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : "";
if (StringUtils.hasText(collectionNameValue)) {
this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue);
}
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) {
this.embeddedValueResolver = embeddedValueResolver;
}
public Integer getMaxInactiveIntervalInSeconds() {
return this.maxInactiveIntervalInSeconds;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public String getCollectionName() {
return this.collectionName;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizers(
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<MongoSession> indexResolver) {
this.indexResolver = indexResolver;
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2019 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.
*/
/**
* Spring Session MongoDB support.
*
* @author Greg Turnquist
*/
@NonNullApi
package org.springframework.session.data.mongo;
import org.springframework.lang.NonNullApi;

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2017 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.mongo;
import java.time.Duration;
import com.mongodb.DBObject;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.session.FindByIndexNameSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Greg Turnquist
*/
public abstract class AbstractMongoSessionConverterTest {
abstract AbstractMongoSessionConverter getMongoSessionConverter();
@Test
void verifyRoundTripSerialization() throws Exception {
// given
MongoSession toSerialize = new MongoSession();
toSerialize.setAttribute("username", "john_the_springer");
// when
DBObject dbObject = convertToDBObject(toSerialize);
MongoSession deserialized = convertToSession(dbObject);
// then
assertThat(deserialized).isEqualToComparingFieldByField(toSerialize);
}
@Test
void verifyRoundTripSecuritySerialization() {
// given
MongoSession toSerialize = new MongoSession();
String principalName = "john_the_springer";
SecurityContextImpl context = new SecurityContextImpl();
context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null));
toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context);
// when
DBObject serialized = convertToDBObject(toSerialize);
MongoSession deserialized = convertToSession(serialized);
// then
assertThat(deserialized).isEqualToComparingOnlyGivenFields(toSerialize, "id", "createdMillis", "accessedMillis",
"intervalSeconds", "expireAt");
SecurityContextImpl springSecurityContextBefore = toSerialize.getAttribute("SPRING_SECURITY_CONTEXT");
SecurityContextImpl springSecurityContextAfter = deserialized.getAttribute("SPRING_SECURITY_CONTEXT");
assertThat(springSecurityContextBefore).isEqualToComparingOnlyGivenFields(springSecurityContextAfter,
"authentication.principal", "authentication.authorities", "authentication.authenticated");
assertThat(springSecurityContextAfter.getAuthentication().getPrincipal()).isEqualTo("john_the_springer");
assertThat(springSecurityContextAfter.getAuthentication().getCredentials()).isNull();
}
@Test
void shouldExtractPrincipalNameFromAttributes() throws Exception {
// given
MongoSession toSerialize = new MongoSession();
String principalName = "john_the_springer";
toSerialize.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
// when
DBObject dbObject = convertToDBObject(toSerialize);
// then
assertThat(dbObject.get("principal")).isEqualTo(principalName);
}
@Test
void shouldExtractPrincipalNameFromAuthentication() throws Exception {
// given
MongoSession toSerialize = new MongoSession();
String principalName = "john_the_springer";
SecurityContextImpl context = new SecurityContextImpl();
context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null));
toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context);
// when
DBObject dbObject = convertToDBObject(toSerialize);
// then
assertThat(dbObject.get("principal")).isEqualTo(principalName);
}
@Test
void sessionWrapperWithNoMaxIntervalShouldFallbackToDefaultValues() {
// given
MongoSession toSerialize = new MongoSession();
DBObject dbObject = convertToDBObject(toSerialize);
Document document = new Document(dbObject.toMap());
document.remove("interval");
// when
MongoSession convertedSession = getMongoSessionConverter().convert(document);
// then
assertThat(convertedSession.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(30));
}
@Nullable
MongoSession convertToSession(DBObject session) {
return (MongoSession) getMongoSessionConverter().convert(session, TypeDescriptor.valueOf(DBObject.class),
TypeDescriptor.valueOf(MongoSession.class));
}
@Nullable
DBObject convertToDBObject(MongoSession session) {
return (DBObject) getMongoSessionConverter().convert(session, TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class));
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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.data.mongo;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.DBObject;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.AssertionsForClassTypes;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.ReflectionUtils;
/**
* @author Jakub Kubrynski
* @author Greg Turnquist
*/
public class JacksonMongoSessionConverterTest extends AbstractMongoSessionConverterTest {
JacksonMongoSessionConverter mongoSessionConverter = new JacksonMongoSessionConverter();
@Override
AbstractMongoSessionConverter getMongoSessionConverter() {
return this.mongoSessionConverter;
}
@Test
void shouldSaveIdField() {
// given
MongoSession session = new MongoSession();
// when
DBObject convert = this.mongoSessionConverter.convert(session);
// then
AssertionsForClassTypes.assertThat(convert.get("_id")).isEqualTo(session.getId());
AssertionsForClassTypes.assertThat(convert.get("id")).isNull();
}
@Test
void shouldQueryAgainstAttribute() throws Exception {
// when
Query cart = this.mongoSessionConverter.getQueryForIndex("cart", "my-cart");
// then
AssertionsForClassTypes.assertThat(cart.getQueryObject().get("attrs.cart")).isEqualTo("my-cart");
}
@Test
void shouldAllowCustomObjectMapper() {
// given
ObjectMapper myMapper = new ObjectMapper();
// when
JacksonMongoSessionConverter converter = new JacksonMongoSessionConverter(myMapper);
// then
Field objectMapperField = ReflectionUtils.findField(JacksonMongoSessionConverter.class, "objectMapper");
ReflectionUtils.makeAccessible(objectMapperField);
ObjectMapper converterMapper = (ObjectMapper) ReflectionUtils.getField(objectMapperField, converter);
AssertionsForClassTypes.assertThat(converterMapper).isEqualTo(myMapper);
}
@Test
void shouldNotAllowNullObjectMapperToBeInjected() {
Assertions.assertThatIllegalArgumentException()
.isThrownBy(() -> new JacksonMongoSessionConverter((ObjectMapper) null));
}
@Test
void shouldSaveExpireAtAsDate() {
// given
MongoSession session = new MongoSession();
// when
DBObject convert = this.mongoSessionConverter.convert(session);
// then
AssertionsForClassTypes.assertThat(convert.get("expireAt")).isInstanceOf(Date.class);
AssertionsForClassTypes.assertThat(convert.get("expireAt")).isEqualTo(session.getExpireAt());
}
@Test
void shouldLoadExpireAtFromDocument() {
// given
Date now = new Date();
HashMap<String, Object> data = new HashMap<>();
data.put("expireAt", now);
data.put("@class", MongoSession.class.getName());
data.put("_id", new ObjectId().toString());
Document document = new Document(data);
// when
MongoSession convertedSession = this.mongoSessionConverter.convert(document);
// then
AssertionsForClassTypes.assertThat(convertedSession).isNotNull();
AssertionsForClassTypes.assertThat(convertedSession.getExpireAt()).isEqualTo(now);
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.data.mongo;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* @author Jakub Kubrynski
* @author Rob Winch
* @author Greg Turnquist
*/
public class JdkMongoSessionConverterTest extends AbstractMongoSessionConverterTest {
Duration inactiveInterval = Duration.ofMinutes(30);
JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(this.inactiveInterval);
@Override
AbstractMongoSessionConverter getMongoSessionConverter() {
return this.mongoSessionConverter;
}
@Test
void constructorNullSerializer() {
assertThatIllegalArgumentException().isThrownBy(
() -> new JdkMongoSessionConverter(null, new DeserializingConverter(), this.inactiveInterval));
}
@Test
void constructorNullDeserializer() {
assertThatIllegalArgumentException().isThrownBy(
() -> new JdkMongoSessionConverter(new SerializingConverter(), null, this.inactiveInterval));
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2014-2017 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.mongo;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.session.FindByIndexNameSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
/**
* Tests for {@link MongoIndexedSessionRepository}.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
@ExtendWith(MockitoExtension.class)
public class MongoIndexedSessionRepositoryTest {
@Mock
private AbstractMongoSessionConverter converter;
@Mock
private MongoOperations mongoOperations;
private MongoIndexedSessionRepository repository;
@BeforeEach
void setUp() {
this.repository = new MongoIndexedSessionRepository(this.mongoOperations);
this.repository.setMongoSessionConverter(this.converter);
}
@Test
void shouldCreateSession() {
// when
MongoSession session = this.repository.createSession();
// then
assertThat(session.getId()).isNotEmpty();
assertThat(session.getMaxInactiveInterval().getSeconds())
.isEqualTo(MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL);
}
@Test
void shouldCreateSessionWhenMaxInactiveIntervalNotDefined() {
// when
this.repository.setMaxInactiveIntervalInSeconds(null);
MongoSession session = this.repository.createSession();
// then
assertThat(session.getId()).isNotEmpty();
assertThat(session.getMaxInactiveInterval().getSeconds())
.isEqualTo(MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL);
}
@Test
void shouldSaveSession() {
// given
MongoSession session = new MongoSession();
BasicDBObject dbSession = new BasicDBObject();
given(this.converter.convert(session, TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class))).willReturn(dbSession);
// when
this.repository.save(session);
// then
verify(this.mongoOperations).save(dbSession, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME);
}
@Test
void shouldGetSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(sessionDocument);
MongoSession session = new MongoSession();
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
MongoSession retrievedSession = this.repository.findById(sessionId);
// then
assertThat(retrievedSession).isEqualTo(session);
}
@Test
void shouldHandleExpiredSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(sessionDocument);
MongoSession session = mock(MongoSession.class);
given(session.isExpired()).willReturn(true);
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
given(session.getId()).willReturn("sessionId");
// when
this.repository.findById(sessionId);
// then
verify(this.mongoOperations).remove(any(Document.class),
eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME));
}
@Test
void shouldDeleteSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
sessionDocument.put("id", sessionId);
MongoSession mongoSession = new MongoSession(sessionId,
MongoIndexedSessionRepository.DEFAULT_INACTIVE_INTERVAL);
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(mongoSession);
given(this.mongoOperations.findById(eq(sessionId), eq(Document.class),
eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME))).willReturn(sessionDocument);
// when
this.repository.deleteById(sessionId);
// then
verify(this.mongoOperations).remove(any(Document.class),
eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME));
}
@Test
void shouldGetSessionsMapByPrincipal() {
// given
String principalNameIndexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
Document document = new Document();
given(this.converter.getQueryForIndex(anyString(), any(Object.class))).willReturn(mock(Query.class));
given(this.mongoOperations.find(any(Query.class), eq(Document.class),
eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)))
.willReturn(Collections.singletonList(document));
String sessionId = UUID.randomUUID().toString();
MongoSession session = new MongoSession(sessionId, 1800);
given(this.converter.convert(document, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
Map<String, MongoSession> sessionsMap = this.repository.findByIndexNameAndIndexValue(principalNameIndexName,
"john");
// then
assertThat(sessionsMap).containsOnlyKeys(sessionId);
assertThat(sessionsMap).containsValues(session);
}
@Test
void shouldReturnEmptyMapForNotSupportedIndex() {
// given
String index = "some_not_supported_index_name";
// when
Map<String, MongoSession> sessionsMap = this.repository.findByIndexNameAndIndexValue(index, "some_value");
// then
assertThat(sessionsMap).isEmpty();
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.data.mongo;
import java.time.Duration;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Greg Turnquist
*/
public class MongoSessionTest {
@Test
void isExpiredWhenIntervalNegativeThenFalse() {
MongoSession session = new MongoSession();
session.setMaxInactiveInterval(Duration.ofSeconds(-1));
session.setLastAccessedTime(Instant.ofEpochMilli(0L));
assertThat(session.isExpired()).isFalse();
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2014-2017 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.mongo;
import java.util.UUID;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.client.result.DeleteResult;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.session.events.SessionDeletedEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
/**
* Tests for {@link ReactiveMongoSessionRepository}.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
@ExtendWith(MockitoExtension.class)
public class ReactiveMongoSessionRepositoryTest {
@Mock
private AbstractMongoSessionConverter converter;
@Mock
private ReactiveMongoOperations mongoOperations;
@Mock
private MongoOperations blockingMongoOperations;
@Mock
private ApplicationEventPublisher eventPublisher;
private ReactiveMongoSessionRepository repository;
@BeforeEach
void setUp() {
this.repository = new ReactiveMongoSessionRepository(this.mongoOperations);
this.repository.setMongoSessionConverter(this.converter);
this.repository.setApplicationEventPublisher(this.eventPublisher);
}
@Test
void shouldCreateSession() {
this.repository.createSession() //
.as(StepVerifier::create) //
.expectNextMatches((mongoSession) -> {
assertThat(mongoSession.getId()).isNotEmpty();
assertThat(mongoSession.getMaxInactiveInterval().getSeconds())
.isEqualTo(ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL);
return true;
}) //
.verifyComplete();
}
@Test
void shouldCreateSessionWhenMaxInactiveIntervalNotDefined() {
// when
this.repository.setMaxInactiveIntervalInSeconds(null);
// then
this.repository.createSession() //
.as(StepVerifier::create) //
.expectNextMatches((mongoSession) -> {
assertThat(mongoSession.getId()).isNotEmpty();
assertThat(mongoSession.getMaxInactiveInterval().getSeconds())
.isEqualTo(ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL);
return true;
}) //
.verifyComplete();
}
@Test
void shouldSaveSession() {
// given
MongoSession session = new MongoSession();
BasicDBObject dbSession = new BasicDBObject();
given(this.converter.convert(session, TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class))).willReturn(dbSession);
given(this.mongoOperations.save(dbSession, "sessions")).willReturn(Mono.just(dbSession));
// when
this.repository.save(session) //
.as(StepVerifier::create) //
.verifyComplete();
verify(this.mongoOperations).save(dbSession, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME);
}
@Test
void shouldGetSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(Mono.just(sessionDocument));
MongoSession session = new MongoSession();
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
this.repository.findById(sessionId) //
.as(StepVerifier::create) //
.expectNext(session) //
.verifyComplete();
}
@Test
void shouldHandleExpiredSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(Mono.just(sessionDocument));
given(this.mongoOperations.remove(sessionDocument, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME))
.willReturn(Mono.just(DeleteResult.acknowledged(1)));
MongoSession session = mock(MongoSession.class);
given(session.isExpired()).willReturn(true);
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
this.repository.findById(sessionId) //
.as(StepVerifier::create) //
.verifyComplete();
// then
verify(this.mongoOperations).remove(any(Document.class),
eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME));
}
@Test
void shouldDeleteSession() {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(Mono.just(sessionDocument));
given(this.mongoOperations.remove(sessionDocument, "sessions"))
.willReturn(Mono.just(DeleteResult.acknowledged(1)));
MongoSession session = mock(MongoSession.class);
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
this.repository.deleteById(sessionId) //
.as(StepVerifier::create) //
.verifyComplete();
verify(this.mongoOperations).remove(any(Document.class),
eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME));
verify(this.eventPublisher).publishEvent(any(SessionDeletedEvent.class));
}
@Test
void shouldInvokeMethodToCreateIndexesImperatively() {
// given
IndexOperations indexOperations = mock(IndexOperations.class);
given(this.blockingMongoOperations.indexOps((String) any())).willReturn(indexOperations);
this.repository.setBlockingMongoOperations(this.blockingMongoOperations);
// when
this.repository.afterPropertiesSet();
// then
verify(this.blockingMongoOperations, times(1)).indexOps((String) any());
verify(this.converter, times(1)).ensureIndexes(indexOperations);
}
}

View File

@@ -0,0 +1,334 @@
/*
* Copyright 2014-2017 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.mongo.config.annotation.web.http;
import java.net.UnknownHostException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.IndexResolver;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JacksonMongoSessionConverter;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
import org.springframework.session.data.mongo.MongoSession;
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.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link MongoHttpSessionConfiguration}.
*
* @author Eddú Meléndez
* @author Vedran Pavic
*/
public class MongoHttpSessionConfigurationTest {
private static final String COLLECTION_NAME = "testSessions";
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@AfterEach
void after() {
if (this.context != null) {
this.context.close();
}
}
@Test
void noMongoOperationsConfiguration() {
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> registerAndRefresh(EmptyConfiguration.class))
.withMessageContaining("mongoSessionRepository");
}
@Test
void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(MongoIndexedSessionRepository.class)).isNotNull();
}
@Test
void customCollectionName() {
registerAndRefresh(CustomCollectionNameConfiguration.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "collectionName")).isEqualTo(COLLECTION_NAME);
}
@Test
void setCustomCollectionName() {
registerAndRefresh(CustomCollectionNameSetConfiguration.class);
MongoHttpSessionConfiguration session = this.context.getBean(MongoHttpSessionConfiguration.class);
assertThat(session).isNotNull();
assertThat(ReflectionTestUtils.getField(session, "collectionName")).isEqualTo(COLLECTION_NAME);
}
@Test
void customMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "maxInactiveIntervalInSeconds"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
void setCustomMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsSetConfiguration.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "maxInactiveIntervalInSeconds"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
void setCustomSessionConverterConfiguration() {
registerAndRefresh(CustomSessionConverterConfiguration.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
AbstractMongoSessionConverter mongoSessionConverter = this.context.getBean(AbstractMongoSessionConverter.class);
assertThat(repository).isNotNull();
assertThat(mongoSessionConverter).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "mongoSessionConverter")).isEqualTo(mongoSessionConverter);
}
@Test
void resolveCollectionNameByPropertyPlaceholder() {
this.context
.setEnvironment(new MockEnvironment().withProperty("session.mongo.collectionName", COLLECTION_NAME));
registerAndRefresh(CustomMongoJdbcSessionConfiguration.class);
MongoHttpSessionConfiguration configuration = this.context.getBean(MongoHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "collectionName")).isEqualTo(COLLECTION_NAME);
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(MongoConfiguration.class, SessionRepositoryCustomizerConfiguration.class);
MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", 10000);
}
@Test
void customIndexResolverConfigurationWithDefaultMongoSessionConverter() {
registerAndRefresh(MongoConfiguration.class,
CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
IndexResolver<MongoSession> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).extracting("mongoSessionConverter").hasFieldOrPropertyWithValue("indexResolver",
indexResolver);
}
@Test
void customIndexResolverConfigurationWithProvidedMongoSessionConverter() {
registerAndRefresh(MongoConfiguration.class,
CustomIndexResolverConfigurationWithProvidedMongoSessionConverter.class);
MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class);
IndexResolver<MongoSession> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).extracting("mongoSessionConverter").hasFieldOrPropertyWithValue("indexResolver",
indexResolver);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
@Configuration
@EnableMongoHttpSession
static class EmptyConfiguration {
}
static class BaseConfiguration {
@Bean
MongoOperations mongoOperations() throws UnknownHostException {
MongoOperations mongoOperations = mock(MongoOperations.class);
IndexOperations indexOperations = mock(IndexOperations.class);
given(mongoOperations.indexOps(anyString())).willReturn(indexOperations);
return mongoOperations;
}
}
@Configuration
@EnableMongoHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
static class MongoConfiguration extends BaseConfiguration {
}
@Configuration
@EnableMongoHttpSession(collectionName = COLLECTION_NAME)
static class CustomCollectionNameConfiguration extends BaseConfiguration {
}
@Configuration
@Import(MongoConfiguration.class)
static class CustomCollectionNameSetConfiguration extends MongoHttpSessionConfiguration {
CustomCollectionNameSetConfiguration() {
setCollectionName(COLLECTION_NAME);
}
}
@Configuration
@EnableMongoHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class CustomMaxInactiveIntervalInSecondsConfiguration extends BaseConfiguration {
}
@Configuration
@Import(MongoConfiguration.class)
static class CustomMaxInactiveIntervalInSecondsSetConfiguration extends MongoHttpSessionConfiguration {
CustomMaxInactiveIntervalInSecondsSetConfiguration() {
setMaxInactiveIntervalInSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
@Configuration
@Import(MongoConfiguration.class)
static class CustomSessionConverterConfiguration extends MongoHttpSessionConfiguration {
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return mock(AbstractMongoSessionConverter.class);
}
}
@Configuration
@EnableMongoHttpSession(collectionName = "${session.mongo.collectionName}")
static class CustomMongoJdbcSessionConfiguration extends BaseConfiguration {
@Bean
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@EnableMongoHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<MongoIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setMaxInactiveIntervalInSeconds(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<MongoIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository.setMaxInactiveIntervalInSeconds(10000);
}
}
@Configuration
@EnableMongoHttpSession
static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<MongoSession> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableMongoHttpSession
static class CustomIndexResolverConfigurationWithProvidedMongoSessionConverter {
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return new JacksonMongoSessionConverter();
}
@Bean
@SuppressWarnings("unchecked")
IndexResolver<MongoSession> indexResolver() {
return mock(IndexResolver.class);
}
}
}

View File

@@ -0,0 +1,385 @@
/*
* Copyright 2017 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.mongo.config.annotation.web.reactive;
import java.lang.reflect.Field;
import java.util.Collections;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.session.IndexResolver;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.data.mongo.AbstractMongoSessionConverter;
import org.springframework.session.data.mongo.JacksonMongoSessionConverter;
import org.springframework.session.data.mongo.JdkMongoSessionConverter;
import org.springframework.session.data.mongo.MongoSession;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.WebSessionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
/**
* Verify various configurations through {@link EnableSpringWebSession}.
*
* @author Greg Turnquist
* @author Vedran Pavić
*/
public class ReactiveMongoWebSessionConfigurationTest {
private AnnotationConfigApplicationContext context;
@AfterEach
void tearDown() {
if (this.context != null) {
this.context.close();
}
}
@Test
void enableSpringWebSessionConfiguresThings() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(GoodConfig.class);
this.context.refresh();
WebSessionManager webSessionManagerFoundByType = this.context.getBean(WebSessionManager.class);
Object webSessionManagerFoundByName = this.context.getBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME);
assertThat(webSessionManagerFoundByType).isNotNull();
assertThat(webSessionManagerFoundByName).isNotNull();
assertThat(webSessionManagerFoundByType).isEqualTo(webSessionManagerFoundByName);
assertThat(this.context.getBean(ReactiveSessionRepository.class)).isNotNull();
}
@Test
void missingReactorSessionRepositoryBreaksAppContext() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(BadConfig.class);
assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(this.context::refresh)
.withMessageContaining("Error creating bean with name 'reactiveMongoSessionRepository'")
.withMessageContaining(
"No qualifying bean of type '" + ReactiveMongoOperations.class.getCanonicalName());
}
@Test
void defaultSessionConverterShouldBeJdkWhenOnClasspath() throws IllegalAccessException {
this.context = new AnnotationConfigApplicationContext();
this.context.register(GoodConfig.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
AbstractMongoSessionConverter converter = findMongoSessionConverter(repository);
assertThat(converter).isOfAnyClassIn(JdkMongoSessionConverter.class);
}
@Test
void overridingMongoSessionConverterWithBeanShouldWork() throws IllegalAccessException {
this.context = new AnnotationConfigApplicationContext();
this.context.register(OverrideSessionConverterConfig.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
AbstractMongoSessionConverter converter = findMongoSessionConverter(repository);
assertThat(converter).isOfAnyClassIn(JacksonMongoSessionConverter.class);
}
@Test
void overridingIntervalAndCollectionNameThroughAnnotationShouldWork() throws IllegalAccessException {
this.context = new AnnotationConfigApplicationContext();
this.context.register(OverrideMongoParametersConfig.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
Field inactiveField = ReflectionUtils.findField(ReactiveMongoSessionRepository.class,
"maxInactiveIntervalInSeconds");
ReflectionUtils.makeAccessible(inactiveField);
Integer inactiveSeconds = (Integer) inactiveField.get(repository);
Field collectionNameField = ReflectionUtils.findField(ReactiveMongoSessionRepository.class, "collectionName");
ReflectionUtils.makeAccessible(collectionNameField);
String collectionName = (String) collectionNameField.get(repository);
assertThat(inactiveSeconds).isEqualTo(123);
assertThat(collectionName).isEqualTo("test-case");
}
@Test
void reactiveAndBlockingMongoOperationsShouldEnsureIndexing() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(ConfigWithReactiveAndImperativeMongoOperations.class);
this.context.refresh();
MongoOperations operations = this.context.getBean(MongoOperations.class);
IndexOperations indexOperations = this.context.getBean(IndexOperations.class);
verify(operations, times(1)).indexOps((String) any());
verify(indexOperations, times(1)).getIndexInfo();
verify(indexOperations, times(1)).ensureIndex(any());
}
@Test
void overrideCollectionAndInactiveIntervalThroughConfigurationOptions() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(CustomizedReactiveConfiguration.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
assertThat(repository.getCollectionName()).isEqualTo("custom-collection");
assertThat(repository.getMaxInactiveIntervalInSeconds()).isEqualTo(123);
}
@Test
void sessionRepositoryCustomizer() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(SessionRepositoryCustomizerConfiguration.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", 10000);
}
@Test
void customIndexResolverConfigurationWithDefaultMongoSessionConverter() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
IndexResolver<MongoSession> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).extracting("mongoSessionConverter").hasFieldOrPropertyWithValue("indexResolver",
indexResolver);
}
@Test
void customIndexResolverConfigurationWithProvidedMongoSessionConverter() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter.class);
this.context.refresh();
ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class);
IndexResolver<MongoSession> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).extracting("mongoSessionConverter").hasFieldOrPropertyWithValue("indexResolver",
indexResolver);
}
/**
* Reflectively extract the {@link AbstractMongoSessionConverter} from the
* {@link ReactiveMongoSessionRepository}. This is to avoid expanding the surface area
* of the API.
*/
private AbstractMongoSessionConverter findMongoSessionConverter(ReactiveMongoSessionRepository repository) {
Field field = ReflectionUtils.findField(ReactiveMongoSessionRepository.class, "mongoSessionConverter");
ReflectionUtils.makeAccessible(field);
try {
return (AbstractMongoSessionConverter) field.get(repository);
}
catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
/**
* A configuration with all the right parts.
*/
@EnableMongoWebSession
static class GoodConfig {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
}
/**
* A configuration where no {@link ReactiveMongoOperations} is defined. It's BAD!
*/
@EnableMongoWebSession
static class BadConfig {
}
@EnableMongoWebSession
static class OverrideSessionConverterConfig {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
@Bean
AbstractMongoSessionConverter mongoSessionConverter() {
return new JacksonMongoSessionConverter();
}
}
@EnableMongoWebSession(maxInactiveIntervalInSeconds = 123, collectionName = "test-case")
static class OverrideMongoParametersConfig {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
}
@EnableMongoWebSession
static class ConfigWithReactiveAndImperativeMongoOperations {
@Bean
ReactiveMongoOperations reactiveMongoOperations() {
return mock(ReactiveMongoOperations.class);
}
@Bean
IndexOperations indexOperations() {
IndexOperations indexOperations = mock(IndexOperations.class);
given(indexOperations.getIndexInfo()).willReturn(Collections.emptyList());
return indexOperations;
}
@Bean
MongoOperations mongoOperations(IndexOperations indexOperations) {
MongoOperations mongoOperations = mock(MongoOperations.class);
given(mongoOperations.indexOps((String) any())).willReturn(indexOperations);
return mongoOperations;
}
}
@EnableSpringWebSession
static class CustomizedReactiveConfiguration extends ReactiveMongoWebSessionConfiguration {
CustomizedReactiveConfiguration() {
this.setCollectionName("custom-collection");
this.setMaxInactiveIntervalInSeconds(123);
}
@Bean
ReactiveMongoOperations reactiveMongoOperations() {
return mock(ReactiveMongoOperations.class);
}
}
@EnableMongoWebSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
@Bean
@Order(0)
ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setMaxInactiveIntervalInSeconds(0);
}
@Bean
@Order(1)
ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository.setMaxInactiveIntervalInSeconds(10000);
}
}
@EnableMongoWebSession
static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
@Bean
@SuppressWarnings("unchecked")
IndexResolver<MongoSession> indexResolver() {
return mock(IndexResolver.class);
}
}
@EnableMongoWebSession
static class CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter {
@Bean
ReactiveMongoOperations operations() {
return mock(ReactiveMongoOperations.class);
}
@Bean
JacksonMongoSessionConverter jacksonMongoSessionConverter() {
return new JacksonMongoSessionConverter();
}
@Bean
@SuppressWarnings("unchecked")
IndexResolver<MongoSession> indexResolver() {
return mock(IndexResolver.class);
}
}
}

View File

@@ -0,0 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%8.-8thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- <logger name="org.springframework.web" level="TRACE" />-->
<!-- <logger name="org.springframework.web.reactive" level="TRACE" />-->
<!-- <logger name="org.springframework.security" level="TRACE" />-->
<!-- <logger name="org.springframework.session" level="TRACE" />-->
<!-- <logger name="org.springframework.data.mongodb" level="TRACE" />-->
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -29,7 +29,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
protected static class BaseConfig {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -473,6 +473,60 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test // gh-1791
void changeSessionIdWhenSessionExpiresThenRemovesAllPrincipalIndexIds() {
RedisSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
String usernameSessionKey = "RedisIndexedSessionRepositoryITests:index:" + INDEX_NAME + ":" + getSecurityName();
RedisSession findById = this.repository.findById(toSave.getId());
String originalFindById = findById.getId();
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).contains(originalFindById);
String changeSessionId = findById.changeSessionId();
findById.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(findById);
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).contains(changeSessionId);
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + changeSessionId;
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
byte[] pattern = new byte[] {};
this.repository.onMessage(message, pattern);
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).isEmpty();
}
@Test
void changeSessionIdWhenPrincipalNameChangesThenNewPrincipalMapsToNewSessionId() {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
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";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -37,6 +37,7 @@ import org.springframework.util.Assert;
* {@link ReactiveRedisOperations}.
*
* @author Vedran Pavic
* @author Kai Zhao
* @since 2.2.0
*/
public class ReactiveRedisSessionRepository
@@ -274,8 +275,14 @@ public class ReactiveRedisSessionRepository
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisSessionRepository.this.sessionRedisOperations.opsForHash()
.putAll(sessionKey, new HashMap<>(this.delta));
Mono<Boolean> setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
getMaxInactiveInterval());
Mono<Boolean> setTtl;
if (getMaxInactiveInterval().getSeconds() >= 0) {
setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
getMaxInactiveInterval());
}
else {
setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.persist(sessionKey);
}
return update.and(setTtl).and((s) -> {
this.delta.clear();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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,6 +18,7 @@ package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -36,6 +37,7 @@ import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
@@ -86,7 +88,7 @@ import org.springframework.util.Assert;
* details.
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
@@ -129,8 +131,8 @@ import org.springframework.util.Assert;
* The {@link RedisIndexedSessionRepository.RedisSession} keeps track of the properties
* that have changed and only updates those. This means if an attribute is written once
* and read many times we only need to write that attribute once. For example, assume the
* session attribute "sessionAttr2" from earlier was updated. The following would be
* executed upon saving:
* session attribute "attrName2" from earlier was updated. The following would be executed
* upon saving:
* </p>
*
* <pre>
@@ -142,7 +144,7 @@ import org.springframework.util.Assert;
* <p>
* When a session is created an event is sent to Redis with the channel of
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the event will be
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the session id. The body of the event will be
* the session that was created.
* </p>
*
@@ -272,10 +274,20 @@ public class RedisIndexedSessionRepository
private String sessionCreatedChannelPrefix;
private byte[] sessionCreatedChannelPrefixBytes;
private String sessionDeletedChannel;
private byte[] sessionDeletedChannelBytes;
private String sessionExpiredChannel;
private byte[] sessionExpiredChannelBytes;
private String expiredKeyPrefix;
private byte[] expiredKeyPrefixBytes;
private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
@@ -381,8 +393,13 @@ public class RedisIndexedSessionRepository
private void configureSessionChannels() {
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
this.sessionCreatedChannelPrefixBytes = this.sessionCreatedChannelPrefix.getBytes();
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
this.sessionDeletedChannelBytes = this.sessionDeletedChannel.getBytes();
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
this.sessionExpiredChannelBytes = this.sessionExpiredChannel.getBytes();
this.expiredKeyPrefix = this.namespace + "sessions:expires:";
this.expiredKeyPrefixBytes = this.expiredKeyPrefix.getBytes();
}
/**
@@ -501,25 +518,24 @@ public class RedisIndexedSessionRepository
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
String channel = new String(messageChannel);
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
if (ByteUtils.startsWith(messageChannel, this.sessionCreatedChannelPrefixBytes)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
handleCreated(loaded, new String(messageChannel));
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
byte[] messageBody = message.getBody();
if (!ByteUtils.startsWith(messageBody, this.expiredKeyPrefixBytes)) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
boolean isDeleted = Arrays.equals(messageChannel, this.sessionDeletedChannelBytes);
if (isDeleted || Arrays.equals(messageChannel, this.sessionExpiredChannelBytes)) {
String body = new String(messageBody);
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
@@ -611,7 +627,7 @@ public class RedisIndexedSessionRepository
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
return this.expiredKeyPrefix;
}
/**
@@ -842,6 +858,11 @@ 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);
}
this.originalSessionId = sessionId;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -50,6 +50,8 @@ final class RedisSessionExpirationPolicy {
private static final Log logger = LogFactory.getLog(RedisSessionExpirationPolicy.class);
private static final String SESSION_EXPIRES_PREFIX = "expires:";
private final RedisOperations<Object, Object> redis;
private final Function<Long, String> lookupExpirationKey;
@@ -67,11 +69,12 @@ final class RedisSessionExpirationPolicy {
void onDelete(Session session) {
long toExpire = roundUpToNextMinute(expiresInMillis(session));
String expireKey = getExpirationKey(toExpire);
this.redis.boundSetOps(expireKey).remove(session.getId());
String entryToRemove = SESSION_EXPIRES_PREFIX + session.getId();
this.redis.boundSetOps(expireKey).remove(entryToRemove);
}
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
String keyToExpire = "expires:" + session.getId();
String keyToExpire = SESSION_EXPIRES_PREFIX + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
if (originalExpirationTimeInMilli != null) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,13 +42,13 @@ import org.springframework.util.Assert;
*/
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
private static final String DEFAULT_KEY_NAMESPACE = "spring:session";
private final RedisOperations<String, Object> sessionRedisOperations;
private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
private String keyNamespace = DEFAULT_KEY_NAMESPACE;
private String keyNamespace = DEFAULT_KEY_NAMESPACE + ":";
private FlushMode flushMode = FlushMode.ON_SAVE;
@@ -76,12 +76,23 @@ public class RedisSessionRepository implements SessionRepository<RedisSessionRep
/**
* Set the key namespace.
* @param keyNamespace the key namespace
* @deprecated since 2.4.0 in favor of {@link #setRedisKeyNamespace(String)}
*/
@Deprecated
public void setKeyNamespace(String keyNamespace) {
Assert.hasText(keyNamespace, "keyNamespace must not be empty");
this.keyNamespace = keyNamespace;
}
/**
* Set the Redis key namespace.
* @param namespace the Redis key namespace
*/
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace must not be empty");
this.keyNamespace = namespace.trim() + ":";
}
/**
* Set the flush mode.
* @param flushMode the flush mode

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -56,6 +56,7 @@ import org.springframework.web.server.session.WebSessionManager;
* More advanced configurations can extend {@link RedisWebSessionConfiguration} instead.
*
* @author Vedran Pavic
* @author Kai Zhao
* @since 2.0.0
* @see EnableSpringWebSession
*/
@@ -68,7 +69,7 @@ public @interface EnableRedisWebSession {
/**
* The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
* This should be a non-negative integer.
* A negative number means permanently valid.
* @return the seconds a session can be inactive before expiring
*/
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -43,12 +43,13 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
* @author Kai Zhao
*/
class ReactiveRedisSessionRepositoryTests {
@@ -137,8 +138,35 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
Map<String, Object> delta = this.delta.getAllValues().get(0);
assertThat(delta.size()).isEqualTo(3);
assertThat(delta.get(RedisSessionMapper.CREATION_TIME_KEY))
.isEqualTo(newSession.getCreationTime().toEpochMilli());
assertThat(delta.get(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY))
.isEqualTo((int) newSession.getMaxInactiveInterval().getSeconds());
assertThat(delta.get(RedisSessionMapper.LAST_ACCESSED_TIME_KEY))
.isEqualTo(newSession.getLastAccessedTime().toEpochMilli());
}
@Test
void saveCustomNegativeMaxInactiveIntervalNewSession() {
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.persist(anyString())).willReturn(Mono.just(true));
MapSession mapSession = new MapSession();
mapSession.setMaxInactiveInterval(Duration.ofSeconds(-1));
RedisSession newSession = this.repository.new RedisSession(mapSession, true);
StepVerifier.create(this.repository.save(newSession)).verifyComplete();
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).persist(anyString());
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
Map<String, Object> delta = this.delta.getAllValues().get(0);
assertThat(delta.size()).isEqualTo(3);
@@ -160,8 +188,8 @@ class ReactiveRedisSessionRepositoryTests {
StepVerifier.create(this.repository.save(session)).verifyComplete();
verify(this.redisOperations).hasKey(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test
@@ -179,8 +207,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0)).isEqualTo(
map(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, session.getLastAccessedTime().toEpochMilli()));
@@ -202,8 +230,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0)).isEqualTo(
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
@@ -225,8 +253,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0))
.isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
@@ -252,8 +280,8 @@ class ReactiveRedisSessionRepositoryTests {
StepVerifier.create(this.repository.deleteById("test")).verifyComplete();
verify(this.redisOperations).delete(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test
@@ -267,8 +295,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).entries(anyString());
verify(this.redisOperations).delete(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test
@@ -291,8 +319,8 @@ class ReactiveRedisSessionRepositoryTests {
StepVerifier.create(this.repository.findById("test")).consumeNextWith((session) -> {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).entries(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
assertThat(session.getId()).isEqualTo(expected.getId());
assertThat(session.getAttributeNames()).isEqualTo(expected.getAttributeNames());
@@ -320,8 +348,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).entries(anyString());
verify(this.redisOperations).delete(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test // gh-1120
@@ -357,8 +385,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(1);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test
@@ -381,8 +409,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(2);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
@Test
@@ -405,8 +433,8 @@ class ReactiveRedisSessionRepositoryTests {
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(3);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
}
private Map<String, Object> map(Object... objects) {

View File

@@ -63,7 +63,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class RedisIndexedSessionRepositoryTests {
@@ -188,7 +188,7 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.save(session);
verifyZeroInteractions(this.redisOperations);
verifyNoMoreInteractions(this.redisOperations);
}
@Test
@@ -511,10 +511,10 @@ class RedisIndexedSessionRepositoryTests {
verify(this.boundHashOperations).entries();
verify(this.publisher).publishEvent(this.event.capture());
assertThat(this.event.getValue().getSessionId()).isEqualTo(deletedId);
verifyZeroInteractions(this.defaultSerializer);
verifyZeroInteractions(this.publisher);
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.defaultSerializer);
verifyNoMoreInteractions(this.publisher);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -534,10 +534,10 @@ class RedisIndexedSessionRepositoryTests {
verify(this.redisOperations).boundHashOps(eq(getKey(deletedId)));
verify(this.boundHashOperations).entries();
verifyZeroInteractions(this.defaultSerializer);
verifyZeroInteractions(this.publisher);
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.defaultSerializer);
verifyNoMoreInteractions(this.publisher);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -561,10 +561,10 @@ class RedisIndexedSessionRepositoryTests {
verify(this.boundHashOperations).entries();
verify(this.publisher).publishEvent(this.event.capture());
assertThat(this.event.getValue().getSessionId()).isEqualTo(expiredId);
verifyZeroInteractions(this.defaultSerializer);
verifyZeroInteractions(this.publisher);
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.defaultSerializer);
verifyNoMoreInteractions(this.publisher);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -584,17 +584,17 @@ class RedisIndexedSessionRepositoryTests {
verify(this.redisOperations).boundHashOps(eq(getKey(expiredId)));
verify(this.boundHashOperations).entries();
verifyZeroInteractions(this.defaultSerializer);
verifyZeroInteractions(this.publisher);
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.defaultSerializer);
verifyNoMoreInteractions(this.publisher);
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
void flushModeOnSaveCreate() {
this.redisRepository.createSession();
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -602,7 +602,7 @@ class RedisIndexedSessionRepositoryTests {
RedisSession session = this.redisRepository.createSession();
session.setAttribute("something", "here");
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -610,7 +610,7 @@ class RedisIndexedSessionRepositoryTests {
RedisSession session = this.redisRepository.createSession();
session.removeAttribute("remove");
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -618,7 +618,7 @@ class RedisIndexedSessionRepositoryTests {
RedisSession session = this.redisRepository.createSession();
session.setLastAccessedTime(Instant.ofEpochMilli(1L));
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -626,7 +626,7 @@ class RedisIndexedSessionRepositoryTests {
RedisSession session = this.redisRepository.createSession();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verifyZeroInteractions(this.boundHashOperations);
verifyNoMoreInteractions(this.boundHashOperations);
}
@Test
@@ -790,7 +790,7 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
verifyNoMoreInteractions(this.publisher);
}
@Test
@@ -808,7 +808,7 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
verifyNoMoreInteractions(this.publisher);
}
@Test
@@ -826,7 +826,7 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
verifyNoMoreInteractions(this.publisher);
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -164,4 +164,11 @@ class RedisSessionExpirationPolicyTests {
verify(this.hashOperations).persist();
}
@Test
void onDeleteRemoveExpirationEntry() {
this.policy.onDelete(this.session);
verify(this.setOperations).remove("expires:" + this.session.getId());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -102,18 +102,36 @@ class RedisSessionRepositoryTests {
assertThat(ReflectionTestUtils.getField(this.sessionRepository, "keyNamespace")).isEqualTo("test:");
}
@Test
void setRedisKeyNamespace_ValidNamespace_ShouldSetNamespace() {
this.sessionRepository.setRedisKeyNamespace("test");
assertThat(ReflectionTestUtils.getField(this.sessionRepository, "keyNamespace")).isEqualTo("test:");
}
@Test
void setKeyNamespace_NullNamespace_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sessionRepository.setKeyNamespace(null))
.withMessage("keyNamespace must not be empty");
}
@Test
void setRedisKeyNamespace_NullNamespace_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sessionRepository.setRedisKeyNamespace(null))
.withMessage("namespace must not be empty");
}
@Test
void setKeyNamespace_EmptyNamespace_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sessionRepository.setKeyNamespace(" "))
.withMessage("keyNamespace must not be empty");
}
@Test
void setRedisKeyNamespace_EmptyNamespace_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sessionRepository.setRedisKeyNamespace(" "))
.withMessage("namespace must not be empty");
}
@Test
void setFlushMode_ValidFlushMode_ShouldSetFlushMode() {
this.sessionRepository.setFlushMode(FlushMode.IMMEDIATE);
@@ -185,7 +203,7 @@ class RedisSessionRepositoryTests {
@Test
void save_NewSessionAndCustomKeyNamespace_ShouldSaveSession() {
this.sessionRepository.setKeyNamespace("custom:");
this.sessionRepository.setRedisKeyNamespace("custom");
RedisSession session = this.sessionRepository.createSession();
this.sessionRepository.save(session);
String key = "custom:sessions:" + session.getId();

View File

@@ -30,7 +30,7 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class RedisHttpSessionConfigurationMockTests {
@@ -53,7 +53,7 @@ class RedisHttpSessionConfigurationMockTests {
init.afterPropertiesSet();
verifyZeroInteractions(this.factory);
verifyNoMoreInteractions(this.factory);
}
@Test

View File

@@ -0,0 +1,9 @@
name: session
title: Spring Session
version: ~
display_version: 2.6
start_page: ROOT:index.adoc
nav:
- modules/ROOT/nav.adoc

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2014-2019 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 docs;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* @author Rob Winch
*
*/
class FindByIndexNameSessionRepositoryTests {
@Mock
FindByIndexNameSessionRepository<Session> sessionRepository;
@Mock
Session session;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
void setUsername() {
// tag::set-username[]
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::set-username[]
}
@Test
@SuppressWarnings("unused")
void findByUsername() {
// tag::findby-username[]
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
// end::findby-username[]
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2014-2019 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 docs;
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.RedisConnectionFactory;
import org.springframework.session.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
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.Mockito.mock;
/**
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
public class HttpSessionConfigurationNoOpConfigureRedisActionXmlTests {
@Autowired
SessionRepositoryFilter<? extends Session> filter;
@Test
void redisConnectionFactoryNotUsedSinceNoValidation() {
assertThat(this.filter).isNotNull();
}
static RedisConnectionFactory connectionFactory() {
return mock(RedisConnectionFactory.class);
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2014-2019 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 docs;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
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.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = this.repository.createSession(); // <2>
// <3>
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); // <4>
S session = this.repository.findById(toSave.getId()); // <5>
// <6>
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = this.repository.createSession(); // <2>
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); // <3>
this.repository.save(toSave); // <4>
S session = this.repository.findById(toSave.getId()); // <5>
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
@SuppressWarnings("unused")
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer()).build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
@SuppressWarnings("unused")
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newHazelcastIndexedSessionRepository() {
// tag::new-hazelcastindexedsessionrepository[]
Config config = new Config();
// ... configure Hazelcast ...
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
// end::new-hazelcastindexedsessionrepository[]
}
@Test
void runSpringHttpSessionConfig() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}

View File

@@ -14,41 +14,48 @@
* limitations under the License.
*/
package org.springframework.session.jdbc;
package docs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.MariaDBContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
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.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.Mockito.mock;
/**
* Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 10.x database.
*
* @author Vedran Pavic
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class MariaDb10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@WebAppConfiguration
class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
@Test
void redisConnectionFactoryNotUsedSinceNoValidation() {
}
@EnableRedisHttpSession
@Configuration
static class Config extends BaseContainerConfig {
static class Config {
// tag::configure-redis-action[]
@Bean
MariaDBContainer databaseContainer() {
MariaDBContainer databaseContainer = DatabaseContainers.mariaDb10();
databaseContainer.start();
return databaseContainer;
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// end::configure-redis-action[]
@Bean
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.mySql();
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2019 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 docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
// tag::class[]
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014-2019 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 docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
// tag::class[]
@EnableSpringWebSession
public class SpringWebSessionConfig {
@Bean
public ReactiveSessionRepository reactiveSessionRepository() {
return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2014-2019 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 docs.http;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
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.security.core.session.SessionDestroyedEvent;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
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.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
* @author Mark Paluch
* @since 1.2
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
public abstract class AbstractHttpSessionListenerTests {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
SecuritySessionDestroyedListener listener;
@Test
void springSessionDestroyedTranslatedToSpringSecurityDestroyed() {
Session session = new MapSession();
this.publisher.publishEvent(new org.springframework.session.events.SessionDestroyedEvent(this, session));
assertThat(this.listener.getEvent().getId()).isEqualTo(session.getId());
}
static RedisConnectionFactory createMockRedisConnection() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
return factory;
}
static class SecuritySessionDestroyedListener implements ApplicationListener<SessionDestroyedEvent> {
private SessionDestroyedEvent event;
/*
* (non-Javadoc)
*
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.
* springframework.context.ApplicationEvent)
*/
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
this.event = event;
}
SessionDestroyedEvent getEvent() {
return this.event;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2014-2019 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 docs.http;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
import org.springframework.session.hazelcast.PrincipalNameExtractor;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
//tag::config[]
@EnableHazelcastHttpSession // <1>
@Configuration
public class HazelcastHttpSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
return Hazelcast.newHazelcastInstance(config); // <4>
}
}
// end::config[]

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2019 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 docs.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Rob Winch
*
*/
@ContextConfiguration(classes = { HttpSessionListenerJavaConfigTests.MockConfig.class, RedisHttpSessionConfig.class })
class HttpSessionListenerJavaConfigTests extends AbstractHttpSessionListenerTests {
@Configuration
static class MockConfig {
@Bean
static RedisConnectionFactory redisConnectionFactory() {
return AbstractHttpSessionListenerTests.createMockRedisConnection();
}
@Bean
SecuritySessionDestroyedListener securitySessionDestroyedListener() {
return new SecuritySessionDestroyedListener();
}
}
}

View File

@@ -14,8 +14,15 @@
* limitations under the License.
*/
package docs;
package docs.http;
public class Docs {
import org.springframework.test.context.ContextConfiguration;
/**
* @author Rob Winch
*
*/
@ContextConfiguration
class HttpSessionListenerXmlTests extends AbstractHttpSessionListenerTests {
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2019 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 docs.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
// tag::config[]
@Configuration
@EnableRedisHttpSession
public class RedisHttpSessionConfig {
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
// ...
}
// end::config[]

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2014-2019 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 docs.security;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
/**
* @author rwinch
*/
@EnableWebSecurity
@EnableSpringHttpSession
public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapter {
// @formatter:off
// tag::http-rememberme[]
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ... additional configuration ...
.rememberMe((rememberMe) -> rememberMe
.rememberMeServices(rememberMeServices())
);
// end::http-rememberme[]
http
.formLogin(Customizer.withDefaults())
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
);
}
// tag::rememberme-bean[]
@Bean
public SpringSessionRememberMeServices rememberMeServices() {
SpringSessionRememberMeServices rememberMeServices =
new SpringSessionRememberMeServices();
// optionally customize
rememberMeServices.setAlwaysRemember(true);
return rememberMeServices;
}
// end::rememberme-bean[]
// @formatter:on
@Override
@Bean
public InMemoryUserDetailsManager userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("user").password("{noop}password").roles("USER").build());
}
@Bean
MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2014-2019 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 docs.security;
import java.time.Duration;
import java.util.Base64;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
/**
* @author rwinch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RememberMeSecurityConfiguration.class)
@WebAppConfiguration
@SuppressWarnings("rawtypes")
class RememberMeSecurityConfigurationTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
SessionRepositoryFilter springSessionRepositoryFilter;
@Autowired
SessionRepository<T> sessions;
private MockMvc mockMvc;
@BeforeEach
void setup() {
// @formatter:off
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(this.springSessionRepositoryFilter)
.apply(springSecurity())
.build();
// @formatter:on
}
@Test
void authenticateWhenSpringSessionRememberMeEnabledThenCookieMaxAgeAndSessionExpirationSet() throws Exception {
// @formatter:off
MvcResult result = this.mockMvc
.perform(formLogin())
.andReturn();
// @formatter:on
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofDays(30));
}
}
// end::class[]

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2014-2019 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 docs.security;
import java.time.Duration;
import java.util.Base64;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
/**
* @author rwinch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
@SuppressWarnings("rawtypes")
class RememberMeSecurityConfigurationXmlTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
SessionRepositoryFilter springSessionRepositoryFilter;
@Autowired
SessionRepository<T> sessions;
private MockMvc mockMvc;
@BeforeEach
void setup() {
// @formatter:off
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(this.springSessionRepositoryFilter)
.apply(springSecurity())
.build();
// @formatter:on
}
@Test
void authenticateWhenSpringSessionRememberMeEnabledThenCookieMaxAgeAndSessionExpirationSet() throws Exception {
// @formatter:off
MvcResult result = this.mockMvc
.perform(formLogin())
.andReturn();
// @formatter:on
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofDays(30));
}
}
// end::class[]

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2014-2019 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 docs.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
/**
* @author Joris Kuipers
*/
// tag::class[]
@Configuration
public class SecurityConfiguration<S extends Session> extends WebSecurityConfigurerAdapter {
@Autowired
private FindByIndexNameSessionRepository<S> sessionRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
// other config goes here...
.sessionManagement((sessionManagement) -> sessionManagement
.maximumSessions(2)
.sessionRegistry(sessionRegistry())
);
// @formatter:on
}
@Bean
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
}
// end::class[]

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014-2019 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 docs.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @author Rob Winch
*/
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messages").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
}
// end::class[]

Some files were not shown because too many files have changed in this diff Show More