Compare commits

..

204 Commits

Author SHA1 Message Date
Eleftheria Stein
3ba76d0375 Add manual trigger to CI workflow
Closes gh-1777
2021-01-19 17:41:19 +01:00
Eleftheria Stein
eb29949996 Release 2.4.2 2021-01-19 14:25:32 +01:00
Eleftheria Stein
02a990d00e Upgrade test dependencies 2021-01-18 13:21:12 +01:00
Eleftheria Stein
56809eacb2 Upgrade Hazelcast to 3.12.11
Closes gh-1766
2021-01-18 13:19:56 +01:00
Eleftheria Stein
4ff8f73b84 Upgrade Spring Data to 2020.0.3
Closes gh-1765
2021-01-18 13:17:38 +01:00
Eleftheria Stein
0ad389633e Upgrade Spring Security to 5.4.2
Closes gh-1764
2021-01-18 13:16:00 +01:00
Eleftheria Stein
5a9046e391 Upgrade Spring Framework to 5.3.3
Closes gh-1763
2021-01-18 13:15:21 +01:00
Eleftheria Stein
690f734307 Upgrade Reactor to 2020.0.3
Closes gh-1762
2021-01-18 13:14:06 +01:00
Eleftheria Stein
823e323f68 Add artifactory credentials to build 2020-11-18 14:39:48 +01:00
Eleftheria Stein
444b5ad85a Rename workflow name to match master 2020-11-10 13:40:22 +01:00
Eleftheria Stein
b4a8c7e516 Create GitHub Actions CI for 2.4.x 2020-11-10 11:52:45 +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
Eleftheria Stein
938fd3c2e5 Next Development Build 2020-01-29 20:55:04 +01:00
Eleftheria Stein
45bb0f9b0c Run deployArtifacts before finalizeDeployArtifacts in build
This commit is needed to fix the release

Resolves: #1574
2020-01-29 16:39:07 +01:00
Eleftheria Stein
cddd84d564 Release 2.2.1.RELEASE 2020-01-28 10:57:10 +01:00
Eleftheria Stein
6931d40e6e Upgrade samples to Spring Boot 2.2.4.RELEASE
Resolves: #1563
2020-01-28 10:26:32 +01:00
Eleftheria Stein
3b672787f3 Upgrade Spring Data to Moore-SR4
Resolves: #1564
2020-01-28 10:22:14 +01:00
Eleftheria Stein
c0ee52b33b Upgrade Reactor to Dysprosium-SR4
Resolves: #1565
2020-01-28 10:21:32 +01:00
Eleftheria Stein
68f8641233 Upgrade Spring Security to 5.2.1.RELEASE
Resolves: #1566
2020-01-28 10:20:48 +01:00
Eleftheria Stein
e7b2af47e1 Upgrade Hazelcast to 3.12.5
Resolves: #1569
2020-01-28 10:19:16 +01:00
Eleftheria Stein
1ad6cbd7f8 Update note in custom-cookie index page
Resolves: gh-1559
2020-01-13 11:57:20 +01:00
Rob Winch
195af52d0b Upgrade to Spring Framework 5.2.2.RELEASE
Fixes gh-1548
2019-12-13 07:06:19 -06:00
Vedran Pavic
bc9d5f1299 Start building against Spring Framework 5.2.2.RELEASE snapshots
See: #1548
2019-11-16 10:20:11 +01:00
Vedran Pavic
3a4345eb6a Upgrade Gradle to 5.6.4 2019-11-15 22:23:23 +01:00
Vedran Pavic
6c41dea893 Polish contribution
Resolves: #1543
2019-11-15 22:10:35 +01:00
Eleftheria Stein
ee1d5b3b3c Document support for SameSite cookie directive
See: #1543
2019-11-15 22:06:13 +01:00
Christoph Dreis
89a4255679 Parse expression only once in PrincipalNameIndexResolver
Resolves: #1539
2019-11-15 12:13:42 +01:00
Rob Winch
6d2e51a0b9 Next Development Build 2019-10-15 15:32:01 -05:00
Rob Winch
798d398d9b Release 2.2.0.RELEASE 2019-10-15 15:31:21 -05:00
Vedran Pavic
085554f56b Polish DefaultCookieSerializer
See: #1514
2019-10-09 08:58:42 +02:00
Vedran Pavic
45b3b35db7 Update Travis CI config 2019-10-08 12:28:42 +02:00
Vedran Pavic
2d06e1159c Improve Hazelcast integration tests
Resolves: #1534
2019-10-08 12:04:42 +02:00
Vedran Pavic
927008bdc8 Ensure session cookie's expires directive uses GMT format
Resolves: #1514
2019-10-07 21:54:00 +02:00
Vedran Pavic
30588dc3c8 Improve Hazelcast client-server topology integration tests
Resolves: #1527
2019-10-06 15:54:02 +02:00
Vedran Pavic
2f79da00dc Upgrade Hazelcast to 3.12.3
Resolves: #1525
2019-10-06 11:41:03 +02:00
Vedran Pavic
e2abe36fa8 Upgrade samples to Spring Boot 2.2.0.RC1
Resolves: #1521
2019-10-03 17:16:59 +02:00
Vedran Pavic
456fd3adb4 Next development version 2019-10-01 06:26:41 +02:00
Vedran Pavic
bd0f474b5b Release 2.2.0.RC1 2019-10-01 06:25:43 +02:00
Vedran Pavic
e5a3933cb6 Upgrade spring-build-conventions to 0.0.27.RELEASE 2019-10-01 06:09:41 +02:00
Vedran Pavic
71e5cc857a Use Jackson 2.10.0 in samples
See: #1508
2019-10-01 00:28:34 +02:00
Vedran Pavic
df455ddc89 Upgrade Spring Security to 5.2.0.RELEASE
Resolves: #1508
2019-09-30 23:55:50 +02:00
Vedran Pavic
eceeaa665d Use Reactor Dysprosium-RELEASE in samples
See: #1509
2019-09-30 21:47:08 +02:00
Vedran Pavic
e6c54d8a75 Upgrade Spring Data to Moore-RELEASE
Resolves: #1507
2019-09-30 21:42:47 +02:00
Vedran Pavic
c88456a183 Rework scheduling configurers into nested configuration
This commit extracts scheduling configurers that are used in Redis and JDBC configurations into nested configuration classes in order to avoid bean method references.

Resolves: #1516
2019-09-30 16:08:55 +02:00
Vedran Pavic
f5abd55394 Ensure proxyBeanMethods is set to false everywhere
This commit sets proxyBeanMethods to false on all @Enable*Session annotations since they are meta-annotated with @Configuration.

See: #1516
2019-09-30 16:08:55 +02:00
Vedran Pavic
b9fd3666b5 Update integration tests 2019-09-30 16:08:20 +02:00
Vedran Pavic
e06ea36ad5 Upgrade test dependencies 2019-09-30 16:08:18 +02:00
Vedran Pavic
0a1701233e Upgrade Spring Framework to 5.2.0.RELEASE
Resolves: #1506
2019-09-30 09:55:05 +02:00
Vedran Pavic
47a4873199 Align TransactionOperations usage with Spring Framework 5.2.0.RELEASE
See: #1506
2019-09-29 22:12:30 +02:00
Vedran Pavic
bd36e115a8 Align with spring-javaformat 0.0.15 2019-09-29 16:16:36 +02:00
Vedran Pavic
ec82336477 Parallelize JDBC integration tests
See: #1505
2019-09-27 07:14:23 +02:00
Vedran Pavic
feaf8780a8 Add support for configuring custom IndexResolver
See: #1467
2019-09-26 22:18:37 +02:00
Vedran Pavic
b357a76ce3 Align Spring Data Redis dependency excludes with Moore-RELEASE
See: #1507
2019-09-26 14:42:22 +02:00
Vedran Pavic
2c6f22afb0 Upgrade Reactor to Dysprosium-RELEASE
Resolves: #1509
2019-09-25 00:30:42 +02:00
Vedran Pavic
34306fd3a0 Fix Gradle dependency caching configuration
See: #1505
2019-09-24 20:50:58 +02:00
Vedran Pavic
a6c1d8eb1d Tweak Gradle JVM memory settings
See: #1505
2019-09-23 22:35:34 +02:00
Vedran Pavic
e48b46a2d5 Improve support for Oracle integration tests
Resolves: #1510
2019-09-23 22:35:33 +02:00
Vedran Pavic
8cc8fbb7fd Harmonize naming of session repositories
Resolves: #1455
2019-09-22 21:47:53 +02:00
Vedran Pavic
96715e04f2 Start building against Reactor Dysprosium-RELEASE snapshots
See: #1509
2019-09-17 22:12:04 +02:00
Vedran Pavic
121a633a40 Optimize project build
This commit optimizes build by:
- configuring Gradle JVM memory settings
- configuring parallel execution
- disabling caching of snapshots

Resolves: #1505
2019-09-17 22:07:44 +02:00
Vedran Pavic
bf31a9b04b Start building against Spring Security 5.2.0.RELEASE snapshots
See: #1508
2019-09-12 22:47:01 +02:00
Vedran Pavic
a209d436d1 Start building against Spring Data Moore-RELEASE snapshots
See: #1507
2019-09-12 22:46:18 +02:00
Vedran Pavic
6c76a1ccdd Start building against Spring Framework 5.2.0.RELEASE snapshots
See: #1506
2019-09-12 22:45:33 +02:00
Vedran Pavic
c974eeb188 Upgrade samples to Spring Boot 2.2.0.M6
Resolves: #1504
2019-09-11 22:38:38 +02:00
Rob Winch
3b5dadb07f Next Development Version 2019-09-06 11:54:55 -05:00
Rob Winch
3e6b3fda0f Release 2.2.0.M4 2019-09-06 11:36:11 -05:00
Rob Winch
840da7fb5a Update to Spring Security 5.2.0.RC1
Fixes gh-1487
2019-09-06 09:31:24 -05:00
Vedran Pavic
560ee5ff4f Upgrade Spring Data to Moore-RC3
Resolves: #1486
2019-09-06 13:17:30 +02:00
Vedran Pavic
072348e28f Upgrade Gradle to 5.6.2 2019-09-05 22:19:39 +02:00
Vedran Pavic
99dfdda7b7 Upgrade Spring Framework to 5.2.0.RC2
Resolves: #1485
2019-09-05 13:12:34 +02:00
Vedran Pavic
18b097d9c7 Upgrade Reactor to Dysprosium-RC1
Resolves: #1498
2019-09-04 07:12:21 +02:00
Vedran Pavic
702a35fac6 Update integration tests 2019-09-03 22:54:57 +02:00
Vedran Pavic
df3e4c5bc1 Add support for customizing session repository before initialization
This commit adds support for customizing session repository implementations (both SessionRepository and ReactiveSessionRepository) before initialization by introducing SessionRepositoryCustomizer and ReactiveSessionRepositoryCustomizer strategies.

Resolves: #1499
2019-09-03 22:17:36 +02:00
Lars Grefer
f746233255 Upgrade Gradle to 5.6.1
Resolves: #1496
2019-08-30 22:44:42 +02:00
Vedran Pavic
f6c82f1eee Improve support for customizing JDBC session store transaction behavior
Resolves: #1469
2019-08-23 23:26:11 +02:00
Josh Cummings
bcdd05a0bc Add OnCommittedResponseWrapper.setContentLengthLong
Add setContentLengthLong tracking to OnCommittedResponseWrapper in
order to detect commits on servlets that use setContentLengthLong to
announce the entity size they are about to write (as used in the
Apache Tomcat's DefaultServlet).

Fixes gh-1489
2019-08-20 13:29:52 -06:00
Vedran Pavic
5d26ab4df4 Add support for AuthenticatedPrincipal in SpringSessionBackedSessionRegistry
Resolves: #1488
2019-08-10 11:23:25 +02:00
Vedran Pavic
e55d86f5e2 Start building against Spring Security 5.2.0.RC1 snapshots
See: #1487
2019-08-07 21:21:34 +02:00
Vedran Pavic
fe480b338c Start building against Spring Data Moore-RC3 snapshots
See: #1486
2019-08-07 21:13:03 +02:00
Vedran Pavic
4b13392430 Start building against Spring Framework 5.2.0.RC2 snapshots
See: #1485
2019-08-07 21:11:34 +02:00
Vedran Pavic
e5d9ce6ead Upgrade samples to Spring Boot 2.2.0.M5
Resolves: #1484
2019-08-06 18:07:18 +02:00
Vedran Pavic
bc1ef4359a Next development version 2019-08-05 22:18:02 +02:00
234 changed files with 8416 additions and 4026 deletions

View File

@@ -1,7 +0,0 @@
<!--
For Security Vulnerabilities, please use https://pivotal.io/security#reporting
-->
<!--
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.
-->

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-security
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?
-->

View File

@@ -0,0 +1,90 @@
name: CI
on:
push:
branches:
- 2.4.x
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
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: |
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
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- 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"
export VERSION_HEADER=$'Version: GnuPG v2\n\n'
export ORG_GRADLE_PROJECT_signingKey=${GPG_PRIVATE_KEY#"$VERSION_HEADER"}
export ORG_GRADLE_PROJECT_signingPassword="$GPG_PASSPHRASE"
./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:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ 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
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- 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 }}

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

@@ -0,0 +1,25 @@
name: PR Build
on: pull_request
jobs:
build:
name: Build
runs-on: ubuntu-latest
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,20 +0,0 @@
language: java
sudo: required
services: docker
jdk: oraclejdk8
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 build --refresh-dependencies --no-daemon

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/master/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

180
Jenkinsfile vendored
View File

@@ -1,180 +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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 finalizeDeployArtifacts --no-daemon --refresh-dependencies --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 --refresh-dependencies --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=master["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:
@@ -18,10 +20,16 @@ Spring Session consists of the following modules:
* 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
== 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/master/CODE_OF_CONDUCT.md[code of conduct]
== Reporting Security Vulnerabilities
Please see our https://github.com/spring-projects/spring-session/security/policy[Security policy].
== Spring Session Project Site

View File

@@ -4,16 +4,17 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.2.0.M4'
springBootVersion = '2.4.0-M4'
}
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release/' }
maven { url 'https://repo.spring.io/plugins-snapshot' }
}
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.26.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.35.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
@@ -28,9 +29,9 @@ subprojects {
plugins.withType(JavaPlugin) {
sourceCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(Test) {
useJUnitPlatform()
}
tasks.withType(Test) {
useJUnitPlatform()
}
}

View File

@@ -7,5 +7,5 @@
<suppress files="[\\/]spring-session-docs[\\/]" checks="InnerTypeLast"/>
<suppress files="[\\/]spring-session-samples[\\/]" checks="Javadoc*"/>
<suppress files="[\\/]spring-session-samples[\\/].+Application\.java" checks="HideUtilityClassConstructor"/>
<suppress files="SessionRepositoryFilterTests\.java" checks="SpringLambda"/>
<suppress files="CookieSerializer\.java" checks="SpringMethodVisibility"/>
</suppressions>

View File

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

View File

@@ -1,34 +1,36 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-M3'
mavenBom 'org.junit:junit-bom:5.5.1'
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RC1'
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-RC2'
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.M4'
mavenBom 'org.testcontainers:testcontainers-bom:1.12.0'
mavenBom 'io.projectreactor:reactor-bom:2020.0.3'
mavenBom 'org.junit:junit-bom:5.7.0'
mavenBom 'org.springframework:spring-framework-bom:5.3.3'
mavenBom 'org.springframework.data:spring-data-bom:2020.0.3'
mavenBom 'org.springframework.security:spring-security-bom:5.4.2'
mavenBom 'org.testcontainers:testcontainers-bom:1.15.1'
}
dependencies {
dependencySet(group: 'com.hazelcast', version: '3.12.2') {
dependencySet(group: 'com.hazelcast', version: '3.12.11') {
entry 'hazelcast'
entry 'hazelcast-client'
}
dependency 'com.h2database:h2:1.4.199'
dependency 'org.aspectj:aspectjweaver:1.9.6'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.0.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.4.1.jre8'
dependency 'com.zaxxer:HikariCP:3.3.1'
dependency 'com.oracle.database.jdbc:ojdbc8:19.8.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.1.8.RELEASE'
dependency 'io.lettuce:lettuce-core:6.0.2.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:4.0.1'
dependency 'mysql:mysql-connector-java:8.0.17'
dependency 'junit:junit:4.13.1'
dependency 'mysql:mysql-connector-java:8.0.22'
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.3'
dependency 'org.mockito:mockito-core:3.0.0'
dependency 'org.postgresql:postgresql:42.2.6'
dependency 'org.assertj:assertj-core:3.18.0'
dependency 'org.hsqldb:hsqldb:2.5.1'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.0'
dependency 'org.mockito:mockito-core:3.5.15'
dependency 'org.postgresql:postgresql:42.2.18'
}
}

View File

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

4
gradlew vendored
View File

@@ -125,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

View File

@@ -5,6 +5,8 @@ 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

@@ -32,7 +32,7 @@ public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexRe
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final SpelExpressionParser parser = new SpelExpressionParser();
private static final Expression expression = new SpelExpressionParser().parseExpression("authentication?.name");
public PrincipalNameIndexResolver() {
super(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
@@ -45,7 +45,6 @@ public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexRe
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;

View File

@@ -0,0 +1,38 @@
/*
* 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 org.springframework.session.config;
import org.springframework.session.ReactiveSessionRepository;
/**
* Strategy that can be used to customize the {@link ReactiveSessionRepository} before it
* is fully initialized, in particular to tune its configuration.
*
* @param <T> the {@link ReactiveSessionRepository} type
* @author Vedran Pavic
* @since 2.2.0
*/
@FunctionalInterface
public interface ReactiveSessionRepositoryCustomizer<T extends ReactiveSessionRepository> {
/**
* Customize the {@link ReactiveSessionRepository}.
* @param sessionRepository the {@link ReactiveSessionRepository} to customize
*/
void customize(T sessionRepository);
}

View File

@@ -0,0 +1,38 @@
/*
* 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 org.springframework.session.config;
import org.springframework.session.SessionRepository;
/**
* Strategy that can be used to customize the {@link SessionRepository} before it is fully
* initialized, in particular to tune its configuration.
*
* @param <T> the {@link SessionRepository} type
* @author Vedran Pavic
* @since 2.2.0
*/
@FunctionalInterface
public interface SessionRepositoryCustomizer<T extends SessionRepository> {
/**
* Customize the {@link SessionRepository}.
* @param sessionRepository the {@link SessionRepository} to customize
*/
void customize(T sessionRepository);
}

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<>());
* }
*
* }
@@ -74,7 +74,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableSpringHttpSession {
}

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<>());
* }
*
* }
@@ -49,7 +49,7 @@ import org.springframework.context.annotation.Import;
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringWebSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableSpringWebSession {
}

View File

@@ -16,14 +16,13 @@
package org.springframework.session.security;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;
@@ -110,13 +109,8 @@ public class SpringSessionBackedSessionRegistry<S extends Session> implements Se
* could be derived
*/
protected String name(Object principal) {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return principal.toString();
// We are reusing the logic from AbstractAuthenticationToken#getName
return new TestingAuthenticationToken(principal, null).getName();
}
}

View File

@@ -16,9 +16,10 @@
package org.springframework.session.web.http;
import java.time.Clock;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Base64;
@@ -62,6 +63,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
domainValid.set('-');
}
private Clock clock = Clock.systemUTC();
private String cookieName = "SESSION";
private Boolean useSecureCookie;
@@ -121,7 +124,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
@@ -132,8 +134,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
int maxAge = getMaxAge(cookieValue);
if (maxAge > -1) {
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
OffsetDateTime expires = (maxAge != 0) ? OffsetDateTime.now().plusSeconds(maxAge)
: Instant.EPOCH.atOffset(ZoneOffset.UTC);
ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
: Instant.EPOCH.atZone(ZoneOffset.UTC);
sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
String domain = getDomainName(request);
@@ -155,7 +157,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
if (this.sameSite != null) {
sb.append("; SameSite=").append(this.sameSite);
}
response.addHeader("Set-Cookie", sb.toString());
}
@@ -259,6 +260,10 @@ public class DefaultCookieSerializer implements CookieSerializer {
}
}
void setClock(Clock clock) {
this.clock = clock.withZone(ZoneOffset.UTC);
}
/**
* Sets if a Cookie marked as secure should be used. The default is to use the value
* of {@link HttpServletRequest#isSecure()}.
@@ -303,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;

View File

@@ -65,11 +65,7 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
this.servletContext = servletContext;
}
public void setSession(S session) {
this.session = session;
}
public S getSession() {
S getSession() {
return this.session;
}
@@ -191,16 +187,16 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
this.invalidated = true;
}
public void setNew(boolean isNew) {
this.old = !isNew;
}
@Override
public boolean isNew() {
checkState();
return !this.old;
}
void markNotNew() {
this.old = true;
}
private void checkState() {
if (this.invalidated) {
throw new IllegalStateException("The HttpSession has already be invalidated.");

View File

@@ -25,9 +25,6 @@ import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base class for response wrappers which encapsulate the logic for handling an event when
* the {@link javax.servlet.http.HttpServletResponse} is committed.
@@ -37,8 +34,6 @@ import org.apache.commons.logging.LogFactory;
*/
abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
private final Log logger = LogFactory.getLog(getClass());
private boolean disableOnCommitted;
/**
@@ -69,6 +64,12 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
super.addHeader(name, value);
}
@Override
public void setContentLengthLong(long len) {
setContentLength(len);
super.setContentLengthLong(len);
}
@Override
public void setContentLength(int len) {
setContentLength((long) len);
@@ -86,7 +87,7 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
* {@link javax.servlet.http.HttpServletResponse} is committed. This can be useful in
* the event that Async Web Requests are made.
*/
public void disableOnResponseCommitted() {
private void disableOnResponseCommitted() {
this.disableOnCommitted = true;
}

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

@@ -292,7 +292,7 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.setNew(false);
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}

View File

@@ -57,6 +57,7 @@ class DelegatingIndexResolverTests {
this.supportedIndex = supportedIndex;
}
@Override
public Map<String, String> resolveIndexesFor(MapSession session) {
return Collections.singletonMap(this.supportedIndex, session.getAttribute(this.supportedIndex));
}

View File

@@ -126,12 +126,12 @@ class EnableSpringHttpSessionCustomCookieSerializerTests {
static class Config {
@Bean
public SessionRepository sessionRepository() {
SessionRepository sessionRepository() {
return mock(SessionRepository.class);
}
@Bean
public CookieSerializer cookieSerializer() {
CookieSerializer cookieSerializer() {
return mock(CookieSerializer.class);
}

View File

@@ -120,7 +120,7 @@ class SpringHttpSessionConfigurationTests {
static class BaseConfiguration {
@Bean
public MapSessionRepository sessionRepository() {
MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
@@ -137,7 +137,7 @@ class SpringHttpSessionConfigurationTests {
static class SessionCookieConfigConfiguration extends BaseConfiguration {
@Bean
public ServletContext servletContext() {
ServletContext servletContext() {
MockServletContext servletContext = new MockServletContext();
servletContext.getSessionCookieConfig().setName("test-name");
servletContext.getSessionCookieConfig().setDomain("test-domain");
@@ -153,7 +153,7 @@ class SpringHttpSessionConfigurationTests {
static class RememberMeServicesConfiguration extends BaseConfiguration {
@Bean
public SpringSessionRememberMeServices rememberMeServices() {
SpringSessionRememberMeServices rememberMeServices() {
return new SpringSessionRememberMeServices();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.session.security;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
@@ -30,6 +31,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.session.SessionInformation;
@@ -104,11 +106,25 @@ class SpringSessionBackedSessionRegistryTest {
}
@Test
void getAllSessions() {
void getAllSessionsForUserDetails() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(PRINCIPAL, true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@Test
void getAllSessionsForAuthenticatedPrincipal() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry
.getAllSessions((AuthenticatedPrincipal) () -> USER_NAME, true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@Test
void getAllSessionsForPrincipal() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(new TestPrincipal(USER_NAME),
true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@@ -159,4 +175,40 @@ class SpringSessionBackedSessionRegistryTest {
when(this.sessionRepository.findByPrincipalName(USER_NAME)).thenReturn(sessions);
}
private static final class TestPrincipal implements Principal {
private final String name;
private TestPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public boolean equals(Object another) {
if (this == another) {
return true;
}
if (another instanceof TestPrincipal) {
return this.name.equals(((TestPrincipal) another).name);
}
return false;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
@Override
public String toString() {
return this.name;
}
}
}

View File

@@ -16,6 +16,11 @@
package org.springframework.session.web.http;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import javax.servlet.http.Cookie;
@@ -71,11 +76,10 @@ class DefaultCookieSerializerTests {
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesSingle(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
}
@@ -84,81 +88,73 @@ class DefaultCookieSerializerTests {
this.sessionId = "&^%$*";
this.serializer.setUseBase64Encoding(true);
this.request.setCookies(new Cookie(this.cookieName, this.sessionId));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesSingleAndInvalidName(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
createCookie(this.cookieName + "INVALID", this.sessionId + "INVALID", useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesMulti(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
String secondSession = "secondSessionId";
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
createCookie(this.cookieName, secondSession, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsExactly(this.sessionId, secondSession);
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesMultiCustomSessionCookieName(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
setCookieName("JSESSIONID");
String secondSession = "secondSessionId";
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
createCookie(this.cookieName, secondSession, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsExactly(this.sessionId, secondSession);
}
// gh-392
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesNullCookieValue(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesNullCookieValueAndJvmRoute(boolean useBase64Encoding) {
this.serializer.setJvmRoute("123");
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieValuesNullCookieValueAndNotNullCookie(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
this.serializer.setJvmRoute("123");
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding),
createCookie(this.cookieName, this.sessionId, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
}
// --- writeCookie ---
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void writeCookie(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookieValue(useBase64Encoding)).isEqualTo(this.sessionId);
}
@@ -167,25 +163,20 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieHttpOnlyDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
void writeCookieHttpOnlySetTrue() {
this.serializer.setUseHttpOnlyCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
void writeCookieHttpOnlySetFalse() {
this.serializer.setUseHttpOnlyCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isFalse();
}
@@ -194,7 +185,6 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieDomainNameDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isNull();
}
@@ -202,9 +192,7 @@ class DefaultCookieSerializerTests {
void writeCookieDomainNameCustom() {
String domainName = "example.com";
this.serializer.setDomainName(domainName);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isEqualTo(domainName);
}
@@ -221,22 +209,18 @@ class DefaultCookieSerializerTests {
void writeCookieDomainNamePattern() {
String domainNamePattern = "^.+?\\.(\\w+\\.[a-z]+)$";
this.serializer.setDomainNamePattern(domainNamePattern);
String[] matchingDomains = { "child.sub.example.com", "www.example.com" };
for (String domain : matchingDomains) {
this.request.setServerName(domain);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isEqualTo("example.com");
this.response = new MockHttpServletResponse();
}
String[] notMatchingDomains = { "example.com", "localhost", "127.0.0.1" };
for (String domain : notMatchingDomains) {
this.request.setServerName(domain);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isNull();
this.response = new MockHttpServletResponse();
}
}
@@ -253,7 +237,6 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieCookieNameDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getName()).isEqualTo("SESSION");
}
@@ -261,9 +244,7 @@ class DefaultCookieSerializerTests {
void writeCookieCookieNameCustom() {
String cookieName = "JSESSIONID";
setCookieName(cookieName);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getName()).isEqualTo(cookieName);
}
@@ -278,18 +259,14 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieCookiePathDefaultEmptyContextPathUsed() {
this.request.setContextPath("");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
@Test
void writeCookieCookiePathDefaultContextPathUsed() {
this.request.setContextPath("/context");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@@ -297,9 +274,7 @@ class DefaultCookieSerializerTests {
void writeCookieCookiePathExplicitNullCookiePathContextPathUsed() {
this.request.setContextPath("/context");
this.serializer.setCookiePath(null);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@@ -307,9 +282,7 @@ class DefaultCookieSerializerTests {
void writeCookieCookiePathExplicitCookiePath() {
this.request.setContextPath("/context");
this.serializer.setCookiePath("/");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
@@ -318,36 +291,45 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieCookieMaxAgeDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(-1);
assertThat(getCookie().getExpires()).isNull();
}
@Test
void writeCookieCookieMaxAgeExplicit() {
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
this.serializer.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(100);
MockCookie cookie = getCookie();
assertThat(cookie.getMaxAge()).isEqualTo(100);
ZonedDateTime expires = cookie.getExpires();
assertThat(expires).isNotNull();
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Mon, 7 Oct 2019 20:11:40 GMT");
}
@Test
void writeCookieCookieMaxAgeExplicitEmptyCookie() {
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
this.serializer.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue(""));
assertThat(getCookie().getMaxAge()).isEqualTo(0);
MockCookie cookie = getCookie();
assertThat(cookie.getMaxAge()).isEqualTo(0);
ZonedDateTime expires = cookie.getExpires();
assertThat(expires).isNotNull();
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Thu, 1 Jan 1970 00:00:00 GMT");
}
@Test
void writeCookieCookieMaxAgeExplicitCookieValue() {
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
CookieValue cookieValue = cookieValue(this.sessionId);
cookieValue.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue);
assertThat(getCookie().getMaxAge()).isEqualTo(100);
MockCookie cookie = getCookie();
assertThat(cookie.getMaxAge()).isEqualTo(100);
ZonedDateTime expires = cookie.getExpires();
assertThat(expires).isNotNull();
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Mon, 7 Oct 2019 20:11:40 GMT");
}
// --- secure ---
@@ -355,7 +337,6 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieDefaultInsecureRequest() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@@ -363,18 +344,14 @@ class DefaultCookieSerializerTests {
void writeCookieSecureSecureRequest() {
this.request.setSecure(true);
this.serializer.setUseSecureCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@Test
void writeCookieSecureInsecureRequest() {
this.serializer.setUseSecureCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@@ -382,65 +359,56 @@ class DefaultCookieSerializerTests {
void writeCookieInsecureSecureRequest() {
this.request.setSecure(true);
this.serializer.setUseSecureCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@Test
void writeCookieInecureInsecureRequest() {
this.serializer.setUseSecureCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
// --- jvmRoute ---
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void writeCookieJvmRoute(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookieValue(useBase64Encoding)).isEqualTo(this.sessionId + "." + jvmRoute);
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieJvmRoute(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(createCookie(this.cookieName, this.sessionId + "." + jvmRoute, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieJvmRouteRouteMissing(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
}
@ParameterizedTest
@ValueSource(strings = { "true", "false" })
@ValueSource(booleans = { true, false })
void readCookieJvmRouteOnlyRoute(boolean useBase64Encoding) {
this.serializer.setUseBase64Encoding(useBase64Encoding);
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(createCookie(this.cookieName, "." + jvmRoute, useBase64Encoding));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly("");
}
@@ -451,7 +419,6 @@ class DefaultCookieSerializerTests {
this.request.setAttribute("rememberMe", true);
this.serializer.setRememberMeRequestAttribute("rememberMe");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(Integer.MAX_VALUE);
}
@@ -462,7 +429,6 @@ class DefaultCookieSerializerTests {
CookieValue cookieValue = cookieValue(this.sessionId);
cookieValue.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue);
assertThat(getCookie().getMaxAge()).isEqualTo(100);
}
@@ -471,7 +437,6 @@ class DefaultCookieSerializerTests {
@Test
void writeCookieDefaultSameSiteLax() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
}
@@ -479,7 +444,6 @@ class DefaultCookieSerializerTests {
void writeCookieSetSameSiteLax() {
this.serializer.setSameSite("Lax");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
}
@@ -487,7 +451,6 @@ class DefaultCookieSerializerTests {
void writeCookieSetSameSiteStrict() {
this.serializer.setSameSite("Strict");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSameSite()).isEqualTo("Strict");
}
@@ -495,11 +458,10 @@ class DefaultCookieSerializerTests {
void writeCookieSetSameSiteNull() {
this.serializer.setSameSite(null);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSameSite()).isNull();
}
public void setCookieName(String cookieName) {
void setCookieName(String cookieName) {
this.cookieName = cookieName;
this.serializer.setCookieName(cookieName);
}

View File

@@ -1100,6 +1100,17 @@ class OnCommittedResponseWrapperTests {
assertThat(this.committed).isTrue();
}
// gh-7261
@Test
void contentLengthLongOutputStreamWriteStringCommits() throws IOException {
String body = "something";
this.response.setContentLengthLong(body.length());
this.response.getOutputStream().print(body);
assertThat(this.committed).isTrue();
}
@Test
void bufferSizeCommitsOnce() throws Exception {
String expected = "1234567890";

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

@@ -1331,11 +1331,8 @@ class SessionRepositoryFilterTests {
// We want the filter to work without any dependencies on Spring
@Test
@SuppressWarnings("unused")
void doesNotImplementOrdered() {
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> {
Ordered o = (Ordered) this.filter;
});
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> Ordered.class.cast(this.filter));
}
@Test

View File

@@ -5,7 +5,6 @@ description = "Spring Session Redis implementation"
dependencies {
compile project(':spring-session-core')
compile ("org.springframework.data:spring-data-redis") {
exclude group: "org.slf4j", module: 'slf4j-api'
exclude group: "org.slf4j", module: 'jcl-over-slf4j'
}

View File

@@ -23,13 +23,13 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
/**
* Base class for {@link RedisOperationsSessionRepository} integration tests.
* Base class for Redis integration tests.
*
* @author Vedran Pavic
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:5.0.5";
private static final String DOCKER_IMAGE = "redis:5.0.10";
protected static class BaseConfig {

View File

@@ -25,6 +25,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.Session;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -34,21 +35,21 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Integration tests for {@link ReactiveRedisOperationsSessionRepository}.
* Integration tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class ReactiveRedisSessionRepositoryITests extends AbstractRedisITests {
@Autowired
private ReactiveRedisOperationsSessionRepository repository;
private ReactiveRedisSessionRepository repository;
@Test
void saves() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
@@ -70,7 +71,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
@Test // gh-1399
void saveMultipleTimes() {
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.createSession().block();
RedisSession session = this.repository.createSession().block();
session.setAttribute("attribute1", "value1");
Mono<Void> save1 = this.repository.save(session);
session.setAttribute("attribute2", "value2");
@@ -80,7 +81,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
@Test
void putAllOnSingleAttrDoesNotRemoveOld() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
toSave.setAttribute("a", "b");
this.repository.save(toSave).block();
@@ -103,13 +104,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
.block();
RedisSession findById = this.repository.findById(toSave.getId()).block();
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
@@ -120,15 +120,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
assertThat(this.repository.findById(originalFindById).block()).isNull();
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
.findById(changeSessionId).block();
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
}
@Test
void changeSessionIdWhenChangeTwice() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
this.repository.save(toSave).block();
@@ -148,12 +147,11 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
.block();
RedisSession findById = this.repository.findById(toSave.getId()).block();
findById.setAttribute(attrName, attrValue);
@@ -164,15 +162,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
assertThat(this.repository.findById(originalFindById).block()).isNull();
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
.findById(changeSessionId).block();
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
}
@Test
void changeSessionIdWhenHasNotSaved() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String originalId = toSave.getId();
toSave.changeSessionId();
@@ -185,7 +182,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
// gh-954
@Test
void changeSessionIdSaveTwice() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String originalId = toSave.getId();
toSave.changeSessionId();
@@ -199,12 +196,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
// gh-1111
@Test
void changeSessionSaveOldSessionInstance() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String sessionId = toSave.getId();
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.findById(sessionId).block();
RedisSession session = this.repository.findById(sessionId).block();
session.changeSessionId();
session.setLastAccessedTime(Instant.now());
this.repository.save(session).block();

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.
@@ -37,7 +37,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
@@ -48,17 +48,23 @@ import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link RedisIndexedSessionRepository}.
*
* @author Rob Winch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
private RedisOperationsSessionRepository repository;
private RedisIndexedSessionRepository repository;
@Autowired
private SessionEventRegistry registry;
@@ -88,7 +94,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
void saves() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
String usernameSessionKey = "RedisOperationsSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
String usernameSessionKey = "RedisIndexedSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
RedisSession toSave = this.repository.createSession();
String expectedAttributeName = "a";
@@ -180,7 +186,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
this.repository.save(toSave);
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
@@ -342,7 +348,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
this.repository.save(toSave);
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
@@ -607,11 +613,11 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisOperationsSessionRepositoryITests")
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
static class Config extends BaseConfig {
@Bean
public SessionEventRegistry sessionEventRegistry() {
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}

View File

@@ -32,7 +32,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -41,17 +41,17 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
* Integration tests for {@link RedisSessionRepository}.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class RedisSessionRepositoryITests extends AbstractRedisITests {
@Autowired
private SimpleRedisOperationsSessionRepository sessionRepository;
private RedisSessionRepository sessionRepository;
@Test
void save_NewSession_ShouldSaveSession() {
@@ -227,11 +227,11 @@ class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
static class Config extends BaseConfig {
@Bean
public SimpleRedisOperationsSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new SimpleRedisOperationsSessionRepository(redisTemplate);
return new RedisSessionRepository(redisTemplate);
}
}

View File

@@ -102,11 +102,11 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
}
}
public boolean receivedEvent() {
boolean receivedEvent() {
return this.receivedEvent;
}
public void setLock(Object lock) {
void setLock(Object lock) {
this.lock = lock;
}
@@ -117,7 +117,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
static class Config extends BaseConfig {
@Bean
public SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
return new SessionExpiredEventRegistry();
}

View File

@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisOperationsSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
class RedisIndexedSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
@Autowired
private SessionRepository<S> sessionRepository;

View File

@@ -88,7 +88,7 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
}
}
public boolean taskDispatched() throws InterruptedException {
boolean taskDispatched() throws InterruptedException {
if (this.taskDispatched != null) {
return this.taskDispatched;
}
@@ -105,12 +105,12 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
static class Config extends BaseConfig {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return new SessionTaskExecutor(Executors.newSingleThreadExecutor());
}
@Bean
public Executor springSessionRedisSubscriptionExecutor() {
Executor springSessionRedisSubscriptionExecutor() {
return new SimpleAsyncTaskExecutor();
}

View File

@@ -16,71 +16,29 @@
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
* {@link ReactiveRedisOperations}.
* This {@link ReactiveSessionRepository} implementation is kept in order to support
* migration to {@link ReactiveRedisSessionRepository} in a backwards compatible manner.
*
* @author Vedran Pavic
* @since 2.0
* @since 2.0.0
* @deprecated since 2.2.0 in favor of {@link ReactiveRedisSessionRepository}
*/
public class ReactiveRedisOperationsSessionRepository
implements ReactiveSessionRepository<ReactiveRedisOperationsSessionRepository.RedisSession> {
@Deprecated
public class ReactiveRedisOperationsSessionRepository extends ReactiveRedisSessionRepository {
/**
* The default namespace for each key and channel in Redis used by Spring Session.
* Create a new {@link ReactiveRedisOperationsSessionRepository} instance.
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
* managing sessions
* @see ReactiveRedisSessionRepository#ReactiveRedisSessionRepository(ReactiveRedisOperations)
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
public ReactiveRedisOperationsSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
}
/**
* Sets the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
super(sessionRedisOperations);
}
/**
@@ -93,226 +51,4 @@ public class ReactiveRedisOperationsSessionRepository
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Returns the {@link ReactiveRedisOperations} used for sessions.
* @return the {@link ReactiveRedisOperations} used for sessions
* @since 2.1.0
*/
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
});
}
@Override
public Mono<Void> save(RedisSession session) {
if (session.isNew) {
return session.save();
}
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
}
@Override
public Mono<RedisSession> findById(String id) {
String sessionKey = getSessionKey(id);
// @formatter:off
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
.filter((map) -> !map.isEmpty())
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map((session) -> new RedisSession(session, false))
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@Override
public Mono<Void> deleteById(String id) {
String sessionKey = getSessionKey(id);
return this.sessionRedisOperations.delete(sessionKey).then();
}
private static String getAttributeKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
private String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
* changed will be persisted.
*/
final class RedisSession implements Session {
private final MapSession cached;
private final Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (ReactiveRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& ReactiveRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getAttributeKey(attributeName), null);
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
private boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
private Mono<Void> save() {
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
}
private Mono<Void> saveDelta() {
if (this.delta.isEmpty()) {
return Mono.empty();
}
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash()
.putAll(sessionKey, new HashMap<>(this.delta));
Mono<Boolean> setTtl = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.expire(sessionKey, getMaxInactiveInterval());
return update.and(setTtl).and((s) -> {
this.delta.clear();
s.onComplete();
}).then();
}
private Mono<Void> saveChangeSessionId() {
if (!hasChangedSessionId()) {
return Mono.empty();
}
String sessionId = getId();
Publisher<Void> replaceSessionId = (s) -> {
this.originalSessionId = sessionId;
s.onComplete();
};
if (this.isNew) {
return Mono.from(replaceSessionId);
}
else {
String originalSessionKey = getSessionKey(this.originalSessionId);
String sessionKey = getSessionKey(sessionId);
return ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.rename(originalSessionKey, sessionKey).and(replaceSessionId);
}
}
}
}

View File

@@ -0,0 +1,312 @@
/*
* 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 org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
* {@link ReactiveRedisOperations}.
*
* @author Vedran Pavic
* @since 2.2.0
*/
public class ReactiveRedisSessionRepository
implements ReactiveSessionRepository<ReactiveRedisSessionRepository.RedisSession> {
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link ReactiveRedisSessionRepository} instance.
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
* managing sessions
*/
public ReactiveRedisSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
}
/**
* Sets the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Returns the {@link ReactiveRedisOperations} used for sessions.
* @return the {@link ReactiveRedisOperations} used for sessions
*/
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
});
}
@Override
public Mono<Void> save(RedisSession session) {
if (session.isNew) {
return session.save();
}
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
}
@Override
public Mono<RedisSession> findById(String id) {
String sessionKey = getSessionKey(id);
// @formatter:off
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
.filter((map) -> !map.isEmpty())
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map((session) -> new RedisSession(session, false))
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@Override
public Mono<Void> deleteById(String id) {
String sessionKey = getSessionKey(id);
return this.sessionRedisOperations.delete(sessionKey).then();
}
private static String getAttributeKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
private String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
* changed will be persisted.
*/
final class RedisSession implements Session {
private final MapSession cached;
private final Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (ReactiveRedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& ReactiveRedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getAttributeKey(attributeName), null);
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
private boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
private Mono<Void> save() {
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
}
private Mono<Void> saveDelta() {
if (this.delta.isEmpty()) {
return Mono.empty();
}
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());
return update.and(setTtl).and((s) -> {
this.delta.clear();
s.onComplete();
}).then();
}
private Mono<Void> saveChangeSessionId() {
if (!hasChangedSessionId()) {
return Mono.empty();
}
String sessionId = getId();
Publisher<Void> replaceSessionId = (s) -> {
this.originalSessionId = sessionId;
s.onComplete();
};
if (this.isNew) {
return Mono.from(replaceSessionId);
}
else {
String originalSessionKey = getSessionKey(this.originalSessionId);
String sessionKey = getSessionKey(sessionId);
return ReactiveRedisSessionRepository.this.sessionRedisOperations.rename(originalSessionKey, sessionKey)
.and(replaceSessionId);
}
}
}
}

View File

@@ -0,0 +1,857 @@
/*
* 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.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
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.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented using
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
* . This implementation supports {@link SessionDeletedEvent} and
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
* </p>
*
* <h2>Creating a new instance</h2>
*
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
*
* // ... configure redisTemplate ...
*
* RedisIndexedSessionRepository redisSessionRepository =
* new RedisIndexedSessionRepository(redisTemplate);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* > Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* The sections below outline how Redis is updated for each operation. An example of
* creating a new session can be found below. The subsequent sections describe the
* details.
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName 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
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <h3>Saving a Session</h3>
*
* <p>
* Each session is stored in Redis as a
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
* example of how each session is stored can be seen below.
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
* </pre>
*
* <p>
* In this example, the session following statements are true about the session:
* </p>
* <ul>
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
* GMT.</li>
* <li>The session expires in 1800 seconds (30 minutes).</li>
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
* 1/1/1970 GMT.</li>
* <li>The session has two attributes. The first is "attrName" with the value of
* "someAttrValue". The second session attribute is named "attrName2" with the value of
* "someAttrValue2".</li>
* </ul>
*
*
* <h3>Optimized Writes</h3>
*
* <p>
* 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:
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
* </pre>
*
* <h3>SessionCreatedEvent</h3>
*
* <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 session id. The body of the event will be
* the session that was created.
* </p>
*
* <p>
* If registered as a {@link MessageListener}, then {@link RedisIndexedSessionRepository}
* will then translate the Redis message into a {@link SessionCreatedEvent}.
* </p>
*
* <h3>Expiration</h3>
*
* <p>
* An expiration is associated to each session using the
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link RedisIndexedSessionRepository.RedisSession#getMaxInactiveInterval()} . For
* example:
* </p>
*
* <pre>
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* </pre>
*
* <p>
* You will note that the expiration that is set is 5 minutes after the session actually
* expires. This is necessary so that the value of the session can be accessed when the
* session expires. An expiration is set on the session itself five minutes after it
* actually expires to ensure it is cleaned up, but only after we perform any necessary
* processing.
* </p>
*
* <p>
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
* be returned. This means there is no need to check the expiration before using a session
* </p>
*
* <p>
* Spring Session relies on the expired and delete
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
* associated with the Session are cleaned up. For example, when using Spring Session's
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
* connections associated with the session to be closed.
* </p>
*
* <p>
* Expiration is not tracked directly on the session key itself since this would mean the
* session data would no longer be available. Instead a special session expires key is
* used. In our example the expires key is:
* </p>
*
* <pre>
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* </pre>
*
* <p>
* When a session expires key is deleted or expires, the keyspace notification triggers a
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
* </p>
*
* <p>
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if the key has not been accessed.
* Specifically the background task that Redis uses to clean up expired keys is a low
* priority task and may not trigger the key expiration. For additional details see
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
* the Redis documentation.
* </p>
*
* <p>
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
* that each key is accessed when it is expected to expire. This means that if the TTL is
* expired on the key, Redis will remove the key and fire the expired event when we try to
* access the key.
* </p>
*
* <p>
* For this reason, each session expiration is also tracked to the nearest minute. This
* allows a background task to access the potentially expired sessions to ensure that
* Redis expired events are fired in a more deterministic fashion. For example:
* </p>
*
* <pre>
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <p>
* The background task will then use these mappings to explicitly request each session
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
* deletes the key for us only if the TTL is expired.
* </p>
* <p>
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
* a race condition that incorrectly identifies a key as expired when it is not. Short of
* using distributed locks (which would kill our performance) there is no way to ensure
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
* the key is only removed if the TTL on that key is expired.
* </p>
*
* @author Rob Winch
* @author Vedran Pavic
* @since 2.2.0
*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
/**
* The default Redis database used by Spring Session.
*/
public static final int DEFAULT_DATABASE = 0;
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private int database = DEFAULT_DATABASE;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
private String sessionCreatedChannelPrefix;
private String sessionDeletedChannel;
private String sessionExpiredChannel;
private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
* sessions. Cannot be null.
*/
public RedisIndexedSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
this::getSessionKey);
configureSessionChannels();
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link SessionDestroyedEvent}. The default is to not publish a
* {@link SessionDestroyedEvent}.
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish {@link SessionDestroyedEvent}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Sets the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Set the {@link IndexResolver} to use.
* @param indexResolver the index resolver
*/
public void setIndexResolver(IndexResolver<Session> indexResolver) {
Assert.notNull(indexResolver, "indexResolver cannot be null");
this.indexResolver = indexResolver;
}
/**
* Sets the default redis serializer. Replaces default serializer which is based on
* {@link JdkSerializationRedisSerializer}.
* @param defaultSerializer the new default redis serializer
*/
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
this.defaultSerializer = defaultSerializer;
}
/**
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
*/
public void setDatabase(int database) {
this.database = database;
configureSessionChannels();
}
private void configureSessionChannels() {
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
}
/**
* Returns the {@link RedisOperations} used for sessions.
* @return the {@link RedisOperations} used for sessions
*/
public RedisOperations<Object, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public void save(RedisSession session) {
session.save();
if (session.isNew) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
@Override
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
String principalKey = getPrincipalKey(indexValue);
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
for (Object id : sessionIds) {
RedisSession session = findById((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
/**
* Gets the session.
* @param id the session id
* @param allowExpired if true, will also include expired sessions that have not been
* deleted. If false, will ensure expired sessions are not returned.
* @return the Redis session
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
}
}
return loaded;
}
@Override
public void deleteById(String sessionId) {
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@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)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
if (isDeleted) {
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(session);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
}
private void handleDeleted(RedisSession session) {
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
publishEvent(new SessionExpiredEvent(this, session));
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
configureSessionChannels();
}
/**
* Gets the Hash key for this session by prefixing it appropriately.
* @param sessionId the session id
* @return the Hash key for this session by prefixing it appropriately.
*/
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
String getPrincipalKey(String principalName) {
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
+ principalName;
}
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getSessionCreatedChannel(String sessionId) {
return getSessionCreatedChannelPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
}
/**
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
* The suffix is the session id of the session that was created.
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
* to
*/
public String getSessionCreatedChannelPrefix() {
return this.sessionCreatedChannelPrefix;
}
/**
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
*/
public String getSessionDeletedChannel() {
return this.sessionDeletedChannel;
}
/**
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
*/
public String getSessionExpiredChannel() {
return this.sessionExpiredChannel;
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
/**
* Gets the key for the specified session attribute.
* @param attributeName the attribute name
* @return the attribute key name
*/
static String getSessionAttrNameKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisIndexedSessionRepository.RedisSession#saveDelta()} is invoked all the
* attributes that have been changed will be persisted.
*
* @author Rob Winch
*/
final class RedisSession implements Session {
private final MapSession cached;
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
saveChangeSessionId();
saveDelta();
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
if (!this.isNew) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
this.originalSessionId = sessionId;
}
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
}

View File

@@ -16,334 +16,31 @@
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
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.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented using
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
* . This implementation supports {@link SessionDeletedEvent} and
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
* </p>
*
* <h2>Creating a new instance</h2>
*
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
*
* // ... configure redisTemplate ...
*
* RedisOperationsSessionRepository redisSessionRepository =
* new RedisOperationsSessionRepository(redisTemplate);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* > Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* The sections below outline how Redis is updated for each operation. An example of
* creating a new session can be found below. The subsequent sections describe the
* details.
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName 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
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <h3>Saving a Session</h3>
*
* <p>
* Each session is stored in Redis as a
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
* example of how each session is stored can be seen below.
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
* </pre>
*
* <p>
* In this example, the session following statements are true about the session:
* </p>
* <ul>
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
* GMT.</li>
* <li>The session expires in 1800 seconds (30 minutes).</li>
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
* 1/1/1970 GMT.</li>
* <li>The session has two attributes. The first is "attrName" with the value of
* "someAttrValue". The second session attribute is named "attrName2" with the value of
* "someAttrValue2".</li>
* </ul>
*
*
* <h3>Optimized Writes</h3>
*
* <p>
* The
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.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:
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
* </pre>
*
* <h3>SessionCreatedEvent</h3>
*
* <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
* the session that was created.
* </p>
*
* <p>
* If registered as a {@link MessageListener}, then
* {@link RedisOperationsSessionRepository} will then translate the Redis message into a
* {@link SessionCreatedEvent}.
* </p>
*
* <h3>Expiration</h3>
*
* <p>
* An expiration is associated to each session using the
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}
* . For example:
* </p>
*
* <pre>
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* </pre>
*
* <p>
* You will note that the expiration that is set is 5 minutes after the session actually
* expires. This is necessary so that the value of the session can be accessed when the
* session expires. An expiration is set on the session itself five minutes after it
* actually expires to ensure it is cleaned up, but only after we perform any necessary
* processing.
* </p>
*
* <p>
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
* be returned. This means there is no need to check the expiration before using a session
* </p>
*
* <p>
* Spring Session relies on the expired and delete
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
* associated with the Session are cleaned up. For example, when using Spring Session's
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
* connections associated with the session to be closed.
* </p>
*
* <p>
* Expiration is not tracked directly on the session key itself since this would mean the
* session data would no longer be available. Instead a special session expires key is
* used. In our example the expires key is:
* </p>
*
* <pre>
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* </pre>
*
* <p>
* When a session expires key is deleted or expires, the keyspace notification triggers a
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
* </p>
*
* <p>
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if the key has not been accessed.
* Specifically the background task that Redis uses to clean up expired keys is a low
* priority task and may not trigger the key expiration. For additional details see
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
* the Redis documentation.
* </p>
*
* <p>
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
* that each key is accessed when it is expected to expire. This means that if the TTL is
* expired on the key, Redis will remove the key and fire the expired event when we try to
* access the key.
* </p>
*
* <p>
* For this reason, each session expiration is also tracked to the nearest minute. This
* allows a background task to access the potentially expired sessions to ensure that
* Redis expired events are fired in a more deterministic fashion. For example:
* </p>
*
* <pre>
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <p>
* The background task will then use these mappings to explicitly request each session
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
* deletes the key for us only if the TTL is expired.
* </p>
* <p>
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
* a race condition that incorrectly identifies a key as expired when it is not. Short of
* using distributed locks (which would kill our performance) there is no way to ensure
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
* the key is only removed if the TTL on that key is expired.
* </p>
* This {@link SessionRepository} implementation is kept in order to support migration to
* {@link RedisIndexedSessionRepository} in a backwards compatible manner.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
* @deprecated since 2.2.0 in favor of {@link RedisIndexedSessionRepository}
*/
public class RedisOperationsSessionRepository
implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
/**
* The default Redis database used by Spring Session.
*/
public static final int DEFAULT_DATABASE = 0;
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private int database = RedisOperationsSessionRepository.DEFAULT_DATABASE;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
private String sessionCreatedChannelPrefix;
private String sessionDeletedChannel;
private String sessionExpiredChannel;
private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
private final IndexResolver<RedisSession> indexResolver;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
@Deprecated
public class RedisOperationsSessionRepository extends RedisIndexedSessionRepository {
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
* sessions. Cannot be null.
* @see RedisIndexedSessionRepository#RedisIndexedSessionRepository(RedisOperations)
*/
public RedisOperationsSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
this::getSessionKey);
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
configureSessionChannels();
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link SessionDestroyedEvent}. The default is to not publish a
* {@link SessionDestroyedEvent}.
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish {@link SessionDestroyedEvent}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Sets the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Sets the default redis serializer. Replaces default serializer which is based on
* {@link JdkSerializationRedisSerializer}.
* @param defaultSerializer the new default redis serializer
*/
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
this.defaultSerializer = defaultSerializer;
super(sessionRedisOperations);
}
/**
@@ -357,517 +54,4 @@ public class RedisOperationsSessionRepository
setFlushMode(redisFlushMode.getFlushMode());
}
/**
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
*/
public void setDatabase(int database) {
this.database = database;
configureSessionChannels();
}
private void configureSessionChannels() {
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
}
/**
* Returns the {@link RedisOperations} used for sessions.
* @return the {@link RedisOperations} used for sessions
* @since 2.0.0
*/
public RedisOperations<Object, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public void save(RedisSession session) {
session.save();
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
@Override
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
String principalKey = getPrincipalKey(indexValue);
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
for (Object id : sessionIds) {
RedisSession session = findById((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
/**
* Gets the session.
* @param id the session id
* @param allowExpired if true, will also include expired sessions that have not been
* deleted. If false, will ensure expired sessions are not returned.
* @return the Redis session
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
}
}
return loaded;
}
@Override
public void deleteById(String sessionId) {
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
@SuppressWarnings("unchecked")
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)) {
// TODO: is this thread safe?
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
if (isDeleted) {
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(session);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
}
private void handleDeleted(RedisSession session) {
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
publishEvent(new SessionExpiredEvent(this, session));
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
configureSessionChannels();
}
/**
* Gets the Hash key for this session by prefixing it appropriately.
* @param sessionId the session id
* @return the Hash key for this session by prefixing it appropriately.
*/
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
String getPrincipalKey(String principalName) {
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
+ principalName;
}
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getSessionCreatedChannel(String sessionId) {
return getSessionCreatedChannelPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
}
/**
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
* The suffix is the session id of the session that was created.
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
* to
*/
public String getSessionCreatedChannelPrefix() {
return this.sessionCreatedChannelPrefix;
}
/**
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
*/
public String getSessionDeletedChannel() {
return this.sessionDeletedChannel;
}
/**
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
*/
public String getSessionExpiredChannel() {
return this.sessionExpiredChannel;
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
/**
* Gets the key for the specified session attribute.
* @param attributeName the attribute name
* @return the attribute key name
*/
static String getSessionAttrNameKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()}
* is invoked all the attributes that have been changed will be persisted.
*
* @author Rob Winch
* @since 1.0
*/
final class RedisSession implements Session {
private final MapSession cached;
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
public boolean isNew() {
return this.isNew;
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if (RedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
saveChangeSessionId();
saveDelta();
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
RedisOperationsSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
if (!isNew()) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
try {
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
this.originalSessionId = sessionId;
}
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
}

View File

@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
/**
* A strategy for expiring {@link RedisSession} instances. This performs two operations:
@@ -64,13 +64,13 @@ final class RedisSessionExpirationPolicy {
this.lookupSessionKey = lookupSessionKey;
}
public void onDelete(Session session) {
void onDelete(Session session) {
long toExpire = roundUpToNextMinute(expiresInMillis(session));
String expireKey = getExpirationKey(toExpire);
this.redis.boundSetOps(expireKey).remove(session.getId());
}
public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
String keyToExpire = "expires:" + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
@@ -117,7 +117,7 @@ final class RedisSessionExpirationPolicy {
return this.lookupSessionKey.apply(sessionId);
}
public void cleanExpiredSessions() {
void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);

View File

@@ -40,8 +40,7 @@ import org.springframework.util.Assert;
* @author Vedran Pavic
* @since 2.2.0
*/
public class SimpleRedisOperationsSessionRepository
implements SessionRepository<SimpleRedisOperationsSessionRepository.RedisSession> {
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
@@ -56,11 +55,11 @@ public class SimpleRedisOperationsSessionRepository
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link SimpleRedisOperationsSessionRepository} instance.
* Create a new {@link RedisSessionRepository} instance.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
* sessions
*/
public SimpleRedisOperationsSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
public RedisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations mut not be null");
this.sessionRedisOperations = sessionRedisOperations;
}
@@ -182,7 +181,7 @@ public class SimpleRedisOperationsSessionRepository
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (SimpleRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
if (this.isNew || (RedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
@@ -201,8 +200,7 @@ public class SimpleRedisOperationsSessionRepository
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& SimpleRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
if (attributeValue != null && RedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
@@ -260,7 +258,7 @@ public class SimpleRedisOperationsSessionRepository
}
private void flushIfRequired() {
if (SimpleRedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
if (RedisSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
@@ -282,8 +280,7 @@ public class SimpleRedisOperationsSessionRepository
if (!this.isNew) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(getId());
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
RedisSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);
}
this.originalSessionId = getId();
}
@@ -294,9 +291,8 @@ public class SimpleRedisOperationsSessionRepository
return;
}
String key = getSessionKey(getId());
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key,
new HashMap<>(this.delta));
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.expireAt(key,
RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap<>(this.delta));
RedisSessionRepository.this.sessionRedisOperations.expireAt(key,
Date.from(Instant.ofEpochMilli(getLastAccessedTime().toEpochMilli())
.plusSeconds(getMaxInactiveInterval().getSeconds())));
this.delta.clear();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2018 the original author or authors.
* 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.
@@ -81,7 +81,7 @@ public class ConfigureNotifyKeyspaceEventsAction implements ConfigureRedisAction
}
catch (InvalidDataAccessApiUsageException ex) {
throw new IllegalStateException(
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent",
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisindexedsessionrepository-sessiondestroyedevent",
ex);
}
}

View File

@@ -24,11 +24,11 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
/**
* Qualifier annotation for a {@link RedisConnectionFactory} to be injected in
* {@link RedisOperationsSessionRepository}.
* {@link RedisIndexedSessionRepository}.
*
* @author Vedran Pavic
* @since 2.0.0

View File

@@ -23,14 +23,18 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
/**
* Annotation used to inject the Redis accessor used by Spring Session's Redis session
* repository.
*
* @author Vedran Pavic
* @see org.springframework.session.data.redis.RedisOperationsSessionRepository#getSessionRedisOperations()
* @see org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository#getSessionRedisOperations()
* @see RedisIndexedSessionRepository#getSessionRedisOperations()
* @see RedisSessionRepository#getSessionRedisOperations()
* @see ReactiveRedisSessionRepository#getSessionRedisOperations()
* @since 2.0.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })

View File

@@ -32,7 +32,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -65,7 +65,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
/**
@@ -85,7 +85,7 @@ public @interface EnableRedisHttpSession {
* the applications and they could function within the same Redis instance.
* @return the unique namespace for keys
*/
String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
/**
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only

View File

@@ -18,8 +18,10 @@ package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
@@ -49,11 +51,14 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -75,15 +80,14 @@ import org.springframework.util.StringValueResolver;
* @since 1.0
*/
@Configuration(proxyBeanMethods = false)
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer {
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
@@ -95,6 +99,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
@@ -103,15 +109,20 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisOperationsSessionRepository sessionRepository() {
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
@@ -123,12 +134,14 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisOperationsSessionRepository sessionRepository) {
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
@@ -209,6 +222,11 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
@@ -221,6 +239,12 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
@@ -255,11 +279,6 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(), this.cleanupCron);
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
@@ -282,7 +301,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
@@ -325,4 +344,25 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final RedisIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -30,7 +30,7 @@ import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.web.server.session.WebSessionManager;
@@ -63,7 +63,7 @@ import org.springframework.web.server.session.WebSessionManager;
@Target(ElementType.TYPE)
@Documented
@Import(RedisWebSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisWebSession {
/**
@@ -83,7 +83,7 @@ public @interface EnableRedisWebSession {
* the applications and they could function within the same Redis instance.
* @return the unique namespace for keys
*/
String redisNamespace() default ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
String redisNamespace() default ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
/**
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only

View File

@@ -16,7 +16,9 @@
package org.springframework.session.data.redis.config.annotation.web.server;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
@@ -36,8 +38,9 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.util.Assert;
@@ -60,7 +63,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
@@ -68,20 +71,23 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private RedisSerializer<Object> defaultRedisSerializer;
private List<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public ReactiveRedisOperationsSessionRepository sessionRepository() {
public ReactiveRedisSessionRepository sessionRepository() {
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate = createReactiveRedisTemplate();
ReactiveRedisOperationsSessionRepository sessionRepository = new ReactiveRedisOperationsSessionRepository(
reactiveRedisTemplate);
ReactiveRedisSessionRepository sessionRepository = new ReactiveRedisSessionRepository(reactiveRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setSaveMode(this.saveMode);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@@ -120,6 +126,12 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;

View File

@@ -33,7 +33,7 @@ import org.springframework.data.redis.core.ReactiveHashOperations;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,11 +46,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link ReactiveRedisOperationsSessionRepository}.
* Tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
*/
class ReactiveRedisOperationsSessionRepositoryTests {
class ReactiveRedisSessionRepositoryTests {
@SuppressWarnings("unchecked")
private ReactiveRedisOperations<String, Object> redisOperations = mock(ReactiveRedisOperations.class);
@@ -61,13 +61,13 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@SuppressWarnings("unchecked")
private ArgumentCaptor<Map<String, Object>> delta = ArgumentCaptor.forClass(Map.class);
private ReactiveRedisOperationsSessionRepository repository;
private ReactiveRedisSessionRepository repository;
private MapSession cached;
@BeforeEach
void setUp() {
this.repository = new ReactiveRedisOperationsSessionRepository(this.redisOperations);
this.repository = new ReactiveRedisSessionRepository(this.redisOperations);
this.cached = new MapSession();
this.cached.setId("session-id");
@@ -77,7 +77,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test
void constructorWithNullReactiveRedisOperations() {
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
.withMessageContaining("sessionRedisOperations cannot be null");
}
@@ -206,7 +206,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0)).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -229,7 +229,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0))
.isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
.isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
}
@Test

View File

@@ -37,8 +37,6 @@ import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.DefaultMessage;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.BoundValueOperations;
@@ -50,7 +48,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent;
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,49 +65,40 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
class RedisOperationsSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
class RedisIndexedSessionRepositoryTests {
@Mock
RedisConnectionFactory factory;
private RedisOperations<Object, Object> redisOperations;
@Mock
RedisConnection connection;
private BoundValueOperations<Object, Object> boundValueOperations;
@Mock
RedisOperations<Object, Object> redisOperations;
private BoundHashOperations<Object, Object, Object> boundHashOperations;
@Mock
BoundValueOperations<Object, Object> boundValueOperations;
private BoundSetOperations<Object, Object> boundSetOperations;
@Mock
BoundHashOperations<Object, Object, Object> boundHashOperations;
private ApplicationEventPublisher publisher;
@Mock
BoundSetOperations<Object, Object> boundSetOperations;
@Mock
ApplicationEventPublisher publisher;
@Mock
RedisSerializer<Object> defaultSerializer;
private RedisSerializer<Object> defaultSerializer;
@Captor
ArgumentCaptor<AbstractSessionEvent> event;
private ArgumentCaptor<AbstractSessionEvent> event;
@Captor
ArgumentCaptor<Map<String, Object>> delta;
private ArgumentCaptor<Map<String, Object>> delta;
private MapSession cached;
private RedisOperationsSessionRepository redisRepository;
private RedisIndexedSessionRepository redisRepository;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.redisRepository = new RedisOperationsSessionRepository(this.redisOperations);
this.redisRepository = new RedisIndexedSessionRepository(this.redisOperations);
this.redisRepository.setDefaultSerializer(this.defaultSerializer);
this.cached = new MapSession();
@@ -277,7 +266,7 @@ class RedisOperationsSessionRepositoryTests {
this.redisRepository.save(session);
assertThat(getDelta()).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -291,7 +280,7 @@ class RedisOperationsSessionRepositoryTests {
this.redisRepository.save(session);
assertThat(getDelta()).isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
assertThat(getDelta()).isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
}
@Test
@@ -320,6 +309,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void delete() {
String attrName = "attrName";
MapSession expected = new MapSession();
@@ -327,7 +317,7 @@ class RedisOperationsSessionRepositoryTests {
expected.setAttribute(attrName, "attrValue");
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
RedisSessionMapper.CREATION_TIME_KEY, expected.getCreationTime().toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) expected.getMaxInactiveInterval().getSeconds(),
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, expected.getLastAccessedTime().toEpochMilli());
@@ -353,6 +343,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionNotFound() {
String id = "abc";
given(this.redisOperations.boundHashOps(getKey(id))).willReturn(this.boundHashOperations);
@@ -362,6 +353,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionFound() {
String attribute1 = "attribute1";
String attribute2 = "attribute2";
@@ -370,8 +362,8 @@ class RedisOperationsSessionRepositoryTests {
expected.setAttribute(attribute1, "test");
expected.setAttribute(attribute2, null);
given(this.redisOperations.boundHashOps(getKey(expected.getId()))).willReturn(this.boundHashOperations);
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attribute1),
expected.getAttribute(attribute1), RedisOperationsSessionRepository.getSessionAttrNameKey(attribute2),
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attribute1),
expected.getAttribute(attribute1), RedisIndexedSessionRepository.getSessionAttrNameKey(attribute2),
expected.getAttribute(attribute2), RedisSessionMapper.CREATION_TIME_KEY,
expected.getCreationTime().toEpochMilli(), RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) expected.getMaxInactiveInterval().getSeconds(), RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
@@ -391,6 +383,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionExpired() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -402,6 +395,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void findByPrincipalNameExpired() {
String expiredId = "expired-id";
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -417,6 +411,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void findByPrincipalName() {
Instant lastAccessed = Instant.now().minusMillis(10);
Instant createdTime = lastAccessed.minusMillis(10);
@@ -446,7 +441,6 @@ class RedisOperationsSessionRepositoryTests {
@Test
void cleanupExpiredSessions() {
String expiredId = "expired-id";
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
Set<Object> expiredIds = new HashSet<>(Arrays.asList("expired-key1", "expired-key2"));
@@ -497,6 +491,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageDeletedSessionFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
@@ -523,6 +518,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageDeletedSessionNotFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
@@ -545,6 +541,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageExpiredSessionFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -571,6 +568,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageExpiredSessionNotFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -677,7 +675,7 @@ class RedisOperationsSessionRepositoryTests {
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -694,10 +692,11 @@ class RedisOperationsSessionRepositoryTests {
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@SuppressWarnings("unchecked")
void flushModeSetMaxInactiveIntervalInSeconds() {
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -736,12 +735,6 @@ class RedisOperationsSessionRepositoryTests {
.withMessage("flushMode cannot be null");
}
@Test
void setRedisFlushModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setRedisFlushMode(null))
.withMessage("redisFlushMode cannot be null");
}
@Test
void changeRedisNamespace() {
String namespace = "foo:bar";

View File

@@ -66,7 +66,7 @@ class RedisSessionExpirationPolicyTests {
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
RedisOperationsSessionRepository repository = new RedisOperationsSessionRepository(this.sessionRedisOperations);
RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(this.sessionRedisOperations);
this.policy = new RedisSessionExpirationPolicy(this.sessionRedisOperations, repository::getExpirationsKey,
repository::getSessionKey);
this.session = new MapSession();

View File

@@ -36,7 +36,7 @@ import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,11 +48,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link SimpleRedisOperationsSessionRepository}.
* Tests for {@link RedisSessionRepository}.
*
* @author Vedran Pavic
*/
class SimpleRedisOperationsSessionRepositoryTests {
class RedisSessionRepositoryTests {
private static final String TEST_SESSION_ID = "session-id";
@@ -67,18 +67,18 @@ class SimpleRedisOperationsSessionRepositoryTests {
@Captor
private ArgumentCaptor<Map<String, Object>> delta;
private SimpleRedisOperationsSessionRepository sessionRepository;
private RedisSessionRepository sessionRepository;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
given(this.sessionRedisOperations.<String, Object>opsForHash()).willReturn(this.sessionHashOperations);
this.sessionRepository = new SimpleRedisOperationsSessionRepository(this.sessionRedisOperations);
this.sessionRepository = new RedisSessionRepository(this.sessionRedisOperations);
}
@Test
void constructor_NullRedisOperations_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
.withMessageContaining("sessionRedisOperations cannot be null");
}

View File

@@ -46,12 +46,12 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
static class Config {
@Bean
public static ConfigureRedisAction configureRedisAction() {
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}

View File

@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideDefaultSerializerTests {
@Bean
@SuppressWarnings("unchecked")
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return mock(RedisSerializer.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
static class Config {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -71,17 +71,17 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
static class Config {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
}
@Bean
public Executor springSessionRedisSubscriptionExecutor() {
Executor springSessionRedisSubscriptionExecutor() {
return mock(Executor.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -29,15 +29,19 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
@@ -54,9 +58,10 @@ import static org.mockito.Mockito.mock;
* @author Mark Paluch
* @author Vedran Pavic
*/
@SuppressWarnings("deprecation")
class RedisHttpSessionConfigurationTests {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private AnnotationConfigApplicationContext context;
@@ -93,8 +98,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -102,8 +106,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -111,8 +114,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -120,8 +122,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -147,14 +148,14 @@ class RedisHttpSessionConfigurationTests {
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@@ -162,7 +163,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -178,7 +179,7 @@ class RedisHttpSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -194,7 +195,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -210,7 +211,7 @@ class RedisHttpSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -229,6 +230,17 @@ class RedisHttpSessionConfigurationTests {
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test // gh-1252
void customRedisMessageListenerContainerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class);
@@ -238,6 +250,14 @@ class RedisHttpSessionConfigurationTests {
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -255,7 +275,7 @@ class RedisHttpSessionConfigurationTests {
static class PropertySourceConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@@ -265,7 +285,7 @@ class RedisHttpSessionConfigurationTests {
static class RedisConfig {
@Bean
public RedisConnectionFactory defaultRedisConnectionFactory() {
RedisConnectionFactory defaultRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -281,6 +301,7 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisHttpSessionConfiguration {
CustomFlushImmediatelySetLegacyConfiguration() {
@@ -297,6 +318,7 @@ class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelyLegacyConfiguration {
}
@@ -335,7 +357,7 @@ class RedisHttpSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -347,7 +369,7 @@ class RedisHttpSessionConfigurationTests {
@Bean
@Primary
public RedisConnectionFactory primaryRedisConnectionFactory() {
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -359,13 +381,13 @@ class RedisHttpSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@Bean
@Primary
public RedisConnectionFactory primaryRedisConnectionFactory() {
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -376,7 +398,7 @@ class RedisHttpSessionConfigurationTests {
static class NamedConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -387,7 +409,7 @@ class RedisHttpSessionConfigurationTests {
static class MultipleConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory secondaryRedisConnectionFactory() {
RedisConnectionFactory secondaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -405,15 +427,45 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@EnableRedisHttpSession
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession
static class CustomRedisMessageListenerContainerConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer redisMessageListenerContainer() {
return new RedisMessageListenerContainer();
}
}
@EnableRedisHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -42,7 +42,6 @@ import static org.mockito.Mockito.mock;
*
* @author Rob Winch
* @author Mark Paluch
* @since 1.0.2
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@@ -63,16 +62,15 @@ class Gh109Tests {
* override sessionRepository construction to set the custom session-timeout
*/
@Bean
public RedisOperationsSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
RedisIndexedSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
ApplicationEventPublisher applicationEventPublisher) {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
sessionRedisTemplate);
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(sessionRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(this.sessionTimeout);
return sessionRepository;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -25,12 +25,14 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.test.util.ReflectionTestUtils;
@@ -68,8 +70,7 @@ class RedisWebSessionConfigurationTests {
void defaultConfiguration() {
registerAndRefresh(RedisConfig.class, DefaultConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
}
@@ -77,8 +78,7 @@ class RedisWebSessionConfigurationTests {
void springSessionRedisOperationsResolvingConfiguration() {
registerAndRefresh(RedisConfig.class, SpringSessionRedisOperationsResolvingConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
ReactiveRedisOperations<String, Object> springSessionRedisOperations = this.context
.getBean(SpringSessionRedisOperationsResolvingConfig.class).getSpringSessionRedisOperations();
@@ -91,8 +91,7 @@ class RedisWebSessionConfigurationTests {
void customNamespace() {
registerAndRefresh(RedisConfig.class, CustomNamespaceConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "namespace")).isEqualTo(REDIS_NAMESPACE + ":");
}
@@ -101,8 +100,7 @@ class RedisWebSessionConfigurationTests {
void customMaxInactiveInterval() {
registerAndRefresh(RedisConfig.class, CustomMaxInactiveIntervalConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -111,23 +109,22 @@ class RedisWebSessionConfigurationTests {
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -143,8 +140,7 @@ class RedisWebSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -160,8 +156,7 @@ class RedisWebSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -177,8 +172,7 @@ class RedisWebSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -198,12 +192,11 @@ class RedisWebSessionConfigurationTests {
}
@Test
@SuppressWarnings("unchecked")
void customRedisSerializerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisSerializerConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
@SuppressWarnings("unchecked")
RedisSerializer<Object> redisSerializer = this.context.getBean("springSessionDefaultRedisSerializer",
RedisSerializer.class);
assertThat(repository).isNotNull();
@@ -222,6 +215,14 @@ class RedisWebSessionConfigurationTests {
"serializer")).isEqualTo(redisSerializer);
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
ReactiveRedisSessionRepository sessionRepository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -231,7 +232,7 @@ class RedisWebSessionConfigurationTests {
static class RedisConfig {
@Bean
public ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -283,7 +284,7 @@ class RedisWebSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -294,7 +295,7 @@ class RedisWebSessionConfigurationTests {
@Bean
@Primary
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -305,13 +306,13 @@ class RedisWebSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@Bean
@Primary
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -321,7 +322,7 @@ class RedisWebSessionConfigurationTests {
static class NamedConnectionFactoryRedisConfig {
@Bean
public ReactiveRedisConnectionFactory redisConnectionFactory() {
ReactiveRedisConnectionFactory redisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -331,7 +332,7 @@ class RedisWebSessionConfigurationTests {
static class MultipleConnectionFactoryRedisConfig {
@Bean
public ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -342,10 +343,28 @@ class RedisWebSessionConfigurationTests {
@Bean
@SuppressWarnings("unchecked")
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return mock(RedisSerializer.class);
}
}
@EnableRedisWebSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -25,7 +25,22 @@ dependencies {
def versions = dependencyManagement.managedVersions
asciidoctorPdf {
clearSources()
sources {
include "index.adoc"
}
}
asciidoctor {
clearSources()
sources {
include "index.adoc"
include "guides/*.adoc"
}
}
asciidoctorj {
def ghTag = snapshotBuild ? 'master' : project.version
def ghUrl = "https://github.com/spring-projects/spring-session/tree/$ghTag"
@@ -46,5 +61,7 @@ asciidoctor {
'spring-session-version': project.version,
'version-milestone': milestoneBuild,
'version-release': releaseBuild,
'version-snapshot': snapshotBuild
'version-snapshot': snapshotBuild,
'highlightjsdir@': "js/highlight",
'docinfodir@': "."
}

View File

@@ -1,11 +1,16 @@
= Spring Session - find by username
Rob Winch
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to find sessions by username.
NOTE: You can find the completed guide in the <<findbyusername-sample, findbyusername application>>.
[#index-link]
link:../index.html[Index]
[[findbyusername-assumptions]]
== Assumptions
@@ -23,7 +28,7 @@ Consider the following scenario:
* User goes to library and authenticates to the application.
* User goes home and realizes they forgot to log out.
* User can log in and terminate the session from the library using clues like the location, created time, last accessed time, and so on.
* User can log in and end the session from the library using clues like the location, created time, last accessed time, and so on.
Would it not be nice if we could let the user invalidate the session at the library from any device with which they authenticate?
This sample demonstrates how this is possible.
@@ -140,5 +145,5 @@ You can emulate the flow we discussed in the <<About the Sample>> section by doi
* Enter the following to log in:
** *Username* _user_
** *Password* _password_
* Terminate your original session.
* End your original session.
* Refresh the original window and see that you are logged out.

View File

@@ -1,11 +1,17 @@
= Spring Session - Spring Boot
Rob Winch, Vedran Pavić
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` when you use Spring Boot.
NOTE: You can find the completed guide in the <<httpsession-jdbc-boot-sample, httpsession-jdbc-boot sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
@@ -46,6 +52,9 @@ spring.session.store-type=jdbc # Session store type.
----
====
If a single Spring Session module is present on the classpath, Spring Boot uses that store implementation automatically.
If you have more than one implementation, you must choose the StoreType that you wish to use to store the sessions, as shows above.
Under the hood, Spring Boot applies configuration that is equivalent to manually adding the `@EnableJdbcHttpSession` annotation.
This creates a Spring bean with the name of `springSessionRepositoryFilter`. That bean implements `Filter`.
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.

View File

@@ -1,11 +1,17 @@
= Spring Session - Spring Boot
Rob Winch, Vedran Pavić
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use Spring Boot.
NOTE: You can find the completed guide in the <<boot-sample, boot sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must ensure your dependencies.
@@ -53,7 +59,7 @@ Further customization is possible by using `application.properties`, as the foll
.src/main/resources/application.properties
----
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
spring.session.redis.flush-mode=on-save # Sessions flush mode.
spring.session.redis.flush-mode=on_save # Sessions flush mode.
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
----
====
@@ -151,6 +157,6 @@ To do so, enter the following into your terminal, being sure to replace `7e8383a
----
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
----
=====
====
Now you can visit the application at http://localhost:8080/ and observe that we are no longer authenticated.

View File

@@ -0,0 +1,66 @@
= Spring Session - WebFlux with Custom Cookie
Eleftheria Stein-Kousathana
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to configure Spring Session to use custom cookies in a WebFlux based application.
The guide assumes you have already set up Spring Session in your project using your chosen data store. For example, link:./boot-redis.html[HttpSession with Redis].
NOTE: You can find the completed guide in the <<webflux-custom-cookie-sample, WebFlux Custom Cookie sample application>>.
[#index-link]
link:../index.html[Index]
[[webflux-custom-cookie-spring-configuration]]
== Spring Boot Configuration
Once you have set up Spring Session, you can customize how the session cookie is written by exposing a `WebSessionIdResolver` as a Spring bean.
Spring Session uses a `CookieWebSessionIdResolver` by default.
Exposing the `WebSessionIdResolver` as a Spring bean augments the existing configuration when you use configurations like `@EnableRedisHttpSession`.
The following example shows how to customize Spring Session's cookie:
====
[source,java]
----
include::{samples-dir}spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/CookieConfig.java[tags=webflux-cookie-serializer]
----
<1> We customize the name of the cookie to be `JSESSIONID`.
<2> We customize the path of the cookie to be `/` (rather than the default of the context root).
<3> We customize the `SameSite` cookie directive to be `Strict`.
====
[[webflux-custom-cookie-sample]]
== `webflux-custom-cookie` Sample Application
This section describes how to work with the `webflux-custom-cookie` sample application.
=== Running the `webflux-custom-cookie` Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
====
----
$ ./gradlew :spring-session-sample-boot-webflux-custom-cookie:bootRun
----
====
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
You should now be able to access the application at http://localhost:8080/
=== Exploring the `webflux-custom-cookie` Sample Application
Now you can use the application. Fill out the form with the following information:
* *Attribute Name:* _username_
* *Attribute Value:* _rob_
Now click the *Set Attribute* button.
You should now see the values displayed in the table.
If you look at the cookies for the application, you can see the cookie is saved to the custom name of `JSESSIONID`.

View File

@@ -1,7 +1,10 @@
= Spring Session - WebSocket
Rob Winch
:toc:
:toc: left
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to ensure that WebSocket messages keep your HttpSession alive.
@@ -12,9 +15,12 @@ Specifically,it does not work with using https://www.jcp.org/en/jsr/detail?id=35
// end::disclaimer[]
[#index-link]
link:../index.html[Index]
== HttpSession Setup
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:httpsession.html[HttpSession Guide].
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:./boot-redis.html[HttpSession with Redis Guide].
Please make sure you have already integrated Spring Session with HttpSession before proceeding.
@@ -53,14 +59,14 @@ What does `AbstractSessionWebSocketMessageBrokerConfigurer` do behind the scenes
* `WebSocketConnectHandlerDecoratorFactory` is added as a `WebSocketHandlerDecoratorFactory` to `WebSocketTransportRegistration`.
This ensures a custom `SessionConnectEvent` is fired that contains the `WebSocketSession`.
The `WebSocketSession` is necessary to terminate any WebSocket connections that are still open when a Spring Session is terminated.
The `WebSocketSession` is necessary to end any WebSocket connections that are still open when a Spring Session is ended.
* `SessionRepositoryMessageInterceptor` is added as a `HandshakeInterceptor` to every `StompWebSocketEndpointRegistration`.
This ensures that the `Session` is added to the WebSocket properties to enable updating the last accessed time.
* `SessionRepositoryMessageInterceptor` is added as a `ChannelInterceptor` to our inbound `ChannelRegistration`.
This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.
* `WebSocketRegistryListener` is created as a Spring bean.
This ensures that we have a mapping of all of the `Session` IDs to the corresponding WebSocket connections.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is terminated.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is ended.
// end::config[]
@@ -125,7 +131,7 @@ You can see that the message is no longer sent.
====
Spring Session expires in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds.
To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions.
This means you need to wait at most two minutes before the WebSocket connection is terminated.
This means you need to wait at most two minutes before the WebSocket connection is closed.
====
You can now try accessing http://localhost:8080/

View File

@@ -0,0 +1,2 @@
<script type="text/javascript" src="../js/tocbot/tocbot.min.js"></script>
<script type="text/javascript" src="../js/toc.js"></script>

View File

@@ -1,12 +1,18 @@
= Spring Session - Custom Cookie
Rob Winch
:toc:
Rob Winch; Eleftheria Stein-Kousathana
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to configure Spring Session to use custom cookies with Java Configuration.
The guide assumes you have already link:./httpsession.html[set up Spring Session in your project].
The guide assumes you have already set up Spring Session in your project using your chosen data store. For example, link:./boot-redis.html[HttpSession with Redis].
NOTE: You can find the completed guide in the <<custom-cookie-sample, Custom Cookie sample application>>.
[#index-link]
link:../index.html[Index]
[[custom-cookie-spring-configuration]]
== Spring Java Configuration
@@ -58,6 +64,9 @@ See `domainNamePattern` as an alternative.
The pattern should provide a single grouping that is used to extract the value of the cookie domain.
If the regular expression does not match, no domain is set and the existing domain is used.
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
* `sameSite`: The value for the `SameSite` cookie directive.
To disable the serialization of the `SameSite` cookie directive, you may set this value to `null`.
Default: `Lax`
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].

View File

@@ -1,12 +1,18 @@
= Spring Session and Spring Security with Hazelcast
Tommy Ludwig; Rob Winch
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session along with Spring Security when you use Hazelcast as your data store.
It assumes that you have already applied Spring Security to your application.
NOTE: You cand find the completed guide in the <<hazelcast-spring-security-sample, Hazelcast Spring Security sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
@@ -91,11 +97,18 @@ The filter is in charge of replacing the `HttpSession` implementation to be back
In this instance, Spring Session is backed by Hazelcast.
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
Spring Session provides `PrincipalNameExtractor` for this purpose.
<3> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
<3> In order to serialize `MapSession` objects efficiently, `HazelcastSessionSerializer` needs to be registered. If this
is not set, Hazelcast will serialize sessions using native Java serialization.
<4> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
By default, the application starts and connects to an embedded instance of Hazelcast.
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
====
NOTE: If `HazelcastSessionSerializer` is preferred, it needs to be configured for all Hazelcast cluster members before they start.
In a Hazelcast cluster, all members should use the same serialization method for sessions. Also, if Hazelcast Client/Server topology
is used, then both members and clients must use the same serialization method. The serializer can be registered via `ClientConfig`
with the same `SerializerConfiguration` of members.
== Servlet Container Initialization
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.

View File

@@ -1,11 +1,17 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch, Vedran Pavić
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` with Java Configuration.
NOTE: You can find the completed guide in the <<httpsession-jdbc-sample, httpsession-jdbc sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
@@ -99,7 +105,7 @@ For additional information on how to configure data access related concerns, see
== Java Servlet Container Initialization
Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
Our <<httpsession-jdbc-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
@@ -122,6 +128,36 @@ Doing so ensures that the Spring bean named `springSessionRepositoryFilter` is r
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to ensure Spring loads our `Config`.
====
== Multiple DataSources
Spring Session provides the `@SpringSessionDataSource` qualifier, allowing you to explicitly declare which `DataSource` bean should be injected in `JdbcIndexedSessionRepository`.
This is particularly useful in scenarios with multiple `DataSource` beans present in the application context.
The following example shows how to do so:
====
.Config.java
[source,java]
----
@EnableJdbcHttpSession
public class Config {
@Bean
@SpringSessionDataSource // <1>
public EmbeddedDatabase firstDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2).addScript("org/springframework/session/jdbc/schema-h2.sql").build();
}
@Bean
public HikariDataSource secondDataSource() {
// ...
}
}
----
<1> This qualifier declares that firstDataSource is to be used by Spring Session.
====
// end::config[]
[[httpsession-jdbc-sample]]

View File

@@ -1,12 +1,18 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch
:toc:
:toc: left
:version-snapshot: true
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with Java Configuration.
NOTE: You can find the completed guide in the <<httpsession-sample, httpsession sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
If you are using Maven, you must add the following dependencies:

View File

@@ -1,11 +1,17 @@
= Spring Session - REST
Rob Winch
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use REST endpoints.
NOTE: You can find the completed guide in the <<rest-sample, rest sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
@@ -241,7 +247,7 @@ $ curl -v http://localhost:8080/ -u user:password
In the output, you should notice the following:
===
====
----
HTTP/1.1 200 OK
...

View File

@@ -1,12 +1,18 @@
= Spring Session and Spring Security
Rob Winch
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session along with Spring Security.
It assumes you have already applied Spring Security to your application.
NOTE: You can find the completed guide in the <<security-sample, security sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
If you use Maven, you must add the following dependencies:

View File

@@ -1,11 +1,17 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch, Vedran Pavić
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage a relational to back a web application's `HttpSession` with XML based configuration.
NOTE: You can find the completed guide in the <<httpsession-jdbc-xml-sample, httpsession-jdbc-xml sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
@@ -68,8 +74,8 @@ You must have the following in your pom.xml:
<url>https://repo.spring.io/libs-milestone</url>
</repository>
----
endif::[]
====
endif::[]
// tag::config[]
@@ -101,7 +107,7 @@ For additional information on how to configure data access-related concerns, see
== XML Servlet Container Initialization
Our <<httpsession-xml-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
Our <<httpsession-jdbc-xml-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, we need to instruct Spring to load our `session.xml` configuration.

View File

@@ -1,11 +1,17 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch
:toc:
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with XML-based configuration.
NOTE: You can find the completed guide in the <<httpsession-xml-sample, httpsession-xml sample application>>.
[#index-link]
link:../index.html[Index]
== Updating Dependencies
Before you use Spring Session, you must update your dependencies.
If you use Maven, you must add the following dependencies:

View File

@@ -1,5 +1,5 @@
= Spring Session
Rob Winch; Vedran Pavić; Jay Bryant
Rob Winch; Vedran Pavić; Jay Bryant; Eleftheria Stein-Kousathana
:doctype: book
:indexdoc-tests: {docs-test-dir}docs/IndexDocTests.java
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
@@ -58,6 +58,10 @@ To get started with Spring Session, the best place to start is our Sample Applic
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store.
| link:guides/boot-jdbc.html[HttpSession with JDBC Guide]
| {gh-samples-url}spring-session-sample-boot-hazelcast[HttpSession with Hazelcast]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Hazelcast.
|
| {gh-samples-url}spring-session-sample-boot-findbyusername[Find by Username]
| Demonstrates how to use Spring Session to find sessions by username.
| link:guides/boot-findbyusername.html[Find by Username Guide]
@@ -70,12 +74,16 @@ To get started with Spring Session, the best place to start is our Sample Applic
| Demonstrates how to use Spring Session to replace the Spring WebFlux's `WebSession` with Redis.
|
| {gh-samples-url}spring-session-sample-boot-webflux-custom-cookie[WebFlux with Custom Cookie]
| Demonstrates how to use Spring Session to customize the Session cookie in a WebFlux based application.
| link:guides/boot-webflux-custom-cookie.html[WebFlux with Custom Cookie Guide]
| {gh-samples-url}spring-session-sample-boot-redis-json[HttpSession with Redis JSON serialization]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using JSON serialization.
|
| {gh-samples-url}spring-session-sample-boot-redis-simple[HttpSession with simple Redis `SessionRepository`]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `SimpleRedisOperationsSessionRepository`.
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `RedisSessionRepository`.
|
|===
@@ -289,7 +297,7 @@ Any method that returns an `HttpSession` is overridden.
All other methods are implemented by `HttpServletRequestWrapper` and delegate to the original `HttpServletRequest` implementation.
We replace the `HttpServletRequest` implementation by using a servlet `Filter` called `SessionRepositoryFilter`.
The pseudocode belows:
The following pseudocode shows how it works:
====
[source, java]
@@ -590,12 +598,13 @@ You can browse the complete link:../../api/[Javadoc] online. The key APIs are de
* <<api-reactivesessionrepository>>
* <<api-enablespringhttpsession>>
* <<api-enablespringwebsession>>
* <<api-redisoperationssessionrepository>>
* <<api-reactiveredisoperationssessionrepository>>
* <<api-redisindexedsessionrepository>>
* <<api-reactiveredissessionrepository>>
* <<api-mapsessionrepository>>
* <<api-reactivemapsessionrepository>>
* <<api-jdbcoperationssessionrepository>>
* <<api-hazelcastsessionrepository>>
* <<api-jdbcindexedsessionrepository>>
* <<api-hazelcastindexedsessionrepository>>
* <<api-cookieserializer>>
[[api-session]]
=== Using `Session`
@@ -727,31 +736,31 @@ Note that no infrastructure for session expirations is configured for you.
This is because things such as session expiration are highly implementation-dependent.
This means that, if you require cleaning up expired sessions, you are responsible for cleaning up the expired sessions.
[[api-redisoperationssessionrepository]]
=== Using `RedisOperationsSessionRepository`
[[api-redisindexedsessionrepository]]
=== Using `RedisIndexedSessionRepository`
`RedisOperationsSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
`RedisIndexedSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
The implementation supports `SessionDestroyedEvent` and `SessionCreatedEvent` through `SessionMessageListener`.
[[api-redisoperationssessionrepository-new]]
==== Instantiating a `RedisOperationsSessionRepository`
[[api-redisindexedsessionrepository-new]]
==== Instantiating a `RedisIndexedSessionRepository`
You can see a typical example of how to create a new instance in the following listing:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-redisoperationssessionrepository]
include::{indexdoc-tests}[tags=new-redisindexedsessionrepository]
----
====
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redisoperationssessionrepository-config]]
[[api-redisindexedsessionrepository-config]]
==== Using `@EnableRedisHttpSession`
In a web environment, the simplest way to create a new `RedisOperationsSessionRepository` is to use `@EnableRedisHttpSession`.
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
You can find complete example usage in the <<samples>>.
You can use the following attributes to customize the configuration:
@@ -766,11 +775,11 @@ You can customize the serialization by creating a bean named `springSessionDefau
==== Redis `TaskExecutor`
`RedisOperationsSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
`RedisIndexedSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
You can customize the way those events are dispatched by creating a bean named `springSessionRedisTaskExecutor`, a bean `springSessionRedisSubscriptionExecutor`, or both.
You can find more details on configuring Redis task executors https://docs.spring.io/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here].
[[api-redisoperationssessionrepository-storage]]
[[api-redisindexedsessionrepository-storage]]
==== Storage Details
The following sections outline how Redis is updated for each operation.
@@ -819,10 +828,10 @@ In the preceding example, the following statements are true about the session:
The first is `attrName`, with a value of `someAttrValue`.
The second session attribute is named `attrName2`, with a value of `someAttrValue2`.
[[api-redisoperationssessionrepository-writes]]
[[api-redisindexedsessionrepository-writes]]
===== Optimized Writes
The `Session` instances managed by `RedisOperationsSessionRepository` keeps track of the properties that have changed and updates only those.
The `Session` instances managed by `RedisIndexedSessionRepository` keeps track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
For example, assume the `sessionAttr2` session attribute from the lsiting in the preceding section was updated.
The following command would be run upon saving:
@@ -833,7 +842,7 @@ HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:a
----
====
[[api-redisoperationssessionrepository-expiration]]
[[api-redisindexedsessionrepository-expiration]]
===== Session Expiration
An expiration is associated with each session by using the `EXPIRE` command, based upon the `Session.getMaxInactiveInterval()`.
@@ -852,7 +861,7 @@ An expiration is set on the session itself five minutes after it actually expire
NOTE: The `SessionRepository.findById(String)` method ensures that no expired sessions are returned.
This means that you need not check the expiration before using a session.
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
`SessionDeletedEvent` or `SessionExpiredEvent` ensure that resources associated with the `Session` are cleaned up.
For example, when you use Spring Session's WebSocket support, the Redis expired or delete event triggers any WebSocket connections associated with the session to be closed.
@@ -893,12 +902,12 @@ Short of using distributed locks (which would kill our performance), there is no
By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
[[api-redisoperationssessionrepository-sessiondestroyedevent]]
[[api-redisindexedsessionrepository-sessiondestroyedevent]]
==== `SessionDeletedEvent` and `SessionExpiredEvent`
`SessionDeletedEvent` and `SessionExpiredEvent` are both types of `SessionDestroyedEvent`.
`RedisOperationsSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
`RedisIndexedSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
This is necessary to ensure resources associated with the `Session` are properly cleaned up.
For example, when integrating with WebSockets, the `SessionDestroyedEvent` is in charge of closing any active WebSocket connections.
@@ -937,15 +946,15 @@ include::{docs-test-resources-dir}docs/HttpSessionConfigurationNoOpConfigureRedi
----
====
[[api-redisoperationssessionrepository-sessioncreatedevent]]
[[api-redisindexedsessionrepository-sessioncreatedevent]]
==== Using `SessionCreatedEvent`
When a session is created, an event is sent to Redis with a channel ID of `spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe`,
where `33fdd1b6-b496-4b33-9f7d-df96679d32fe` is the session ID. The body of the event is the session that was created.
If registered as a `MessageListener` (the default), `RedisOperationsSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
If registered as a `MessageListener` (the default), `RedisIndexedSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
[[api-redisoperationssessionrepository-cli]]
[[api-redisindexedsessionrepository-cli]]
==== Viewing the Session in Redis
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
@@ -980,30 +989,30 @@ redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed
----
====
[[api-reactiveredisoperationssessionrepository]]
=== Using `ReactiveRedisOperationsSessionRepository`
[[api-reactiveredissessionrepository]]
=== Using `ReactiveRedisSessionRepository`
`ReactiveRedisOperationsSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
`ReactiveRedisSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
In a web environment, this is typically used in combination with `WebSessionStore`.
[[api-reactiveredisoperationssessionrepository-new]]
==== Instantiating a `ReactiveRedisOperationsSessionRepository`
[[api-reactiveredissessionrepository-new]]
==== Instantiating a `ReactiveRedisSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-reactiveredisoperationssessionrepository]
include::{indexdoc-tests}[tags=new-reactiveredissessionrepository]
----
====
For additional information on how to create a `ReactiveRedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-reactiveredisoperationssessionrepository-config]]
[[api-reactiveredissessionrepository-config]]
==== Using `@EnableRedisWebSession`
In a web environment, the simplest way to create a new `ReactiveRedisOperationsSessionRepository` is to use `@EnableRedisWebSession`.
In a web environment, the simplest way to create a new `ReactiveRedisSessionRepository` is to use `@EnableRedisWebSession`.
You can use the following attributes to customize the configuration:
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds
@@ -1011,13 +1020,13 @@ You can use the following attributes to customize the configuration:
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `ReactiveSessionRepository`.
A value of `FlushMode.IMMEDIATE` writes to Redis as soon as possible.
[[api-reactiveredisoperationssessionrepository-writes]]
[[api-reactiveredissessionrepository-writes]]
===== Optimized Writes
The `Session` instances managed by `ReactiveRedisOperationsSessionRepository` keep track of the properties that have changed and updates only those.
The `Session` instances managed by `ReactiveRedisSessionRepository` keep track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
[[api-reactiveredisoperationssessionrepository-cli]]
[[api-reactiveredissessionrepository-cli]]
==== Viewing the Session in Redis
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
@@ -1101,31 +1110,31 @@ The `ReactiveMapSessionRepository` allows for persisting `Session` in a `Map`, w
You can use the implementation with a `ConcurrentHashMap` as a testing or convenience mechanism.
Alternatively, you can use it with distributed `Map` implementations, with the requirement that the supplied `Map` must be non-blocking.
[[api-jdbcoperationssessionrepository]]
=== Using `JdbcOperationsSessionRepository`
[[api-jdbcindexedsessionrepository]]
=== Using `JdbcIndexedSessionRepository`
`JdbcOperationsSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
`JdbcIndexedSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
Note that this implementation does not support publishing of session events.
[[api-jdbcoperationssessionrepository-new]]
==== Instantiating a `JdbcOperationsSessionRepository`
[[api-jdbcindexedsessionrepository-new]]
==== Instantiating a `JdbcIndexedSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-jdbcoperationssessionrepository]
include::{indexdoc-tests}[tags=new-jdbcindexedsessionrepository]
----
====
For additional information on how to create and configure `JdbcTemplate` and `PlatformTransactionManager`, see the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
[[api-jdbcoperationssessionrepository-config]]
[[api-jdbcindexedsessionrepository-config]]
==== Using `@EnableJdbcHttpSession`
In a web environment, the simplest way to create a new `JdbcOperationsSessionRepository` is to use `@EnableJdbcHttpSession`.
In a web environment, the simplest way to create a new `JdbcIndexedSessionRepository` is to use `@EnableJdbcHttpSession`.
You can find complete example usage in the <<samples>>
You can use the following attributes to customize the configuration:
@@ -1142,7 +1151,7 @@ You can customize the default serialization and deserialization of the session b
When working in a typical Spring environment, the default `ConversionService` bean (named `conversionService`) is automatically picked up and used for serialization and deserialization.
However, you can override the default `ConversionService` by providing a bean named `springSessionConversionService`.
[[api-jdbcoperationssessionrepository-storage]]
[[api-jdbcindexedsessionrepository-storage]]
==== Storage Details
By default, this implementation uses `SPRING_SESSION` and `SPRING_SESSION_ATTRIBUTES` tables to store sessions.
@@ -1172,24 +1181,24 @@ include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schem
==== Transaction Management
All JDBC operations in `JdbcOperationsSessionRepository` are executed in a transactional manner.
Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
All JDBC operations in `JdbcIndexedSessionRepository` are performed in a transactional manner.
Transactions are performed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
[[api-hazelcastsessionrepository]]
=== Using `HazelcastSessionRepository`
[[api-hazelcastindexedsessionrepository]]
=== Using `HazelcastIndexedSessionRepository`
`HazelcastSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
`HazelcastIndexedSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
[[api-hazelcastsessionrepository-new]]
==== Instantiating a `HazelcastSessionRepository`
[[api-hazelcastindexedsessionrepository-new]]
==== Instantiating a `HazelcastIndexedSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-hazelcastsessionrepository]
include::{indexdoc-tests}[tags=new-hazelcastindexedsessionrepository]
----
====
@@ -1230,6 +1239,68 @@ Note that if you use Hazelcast's `MapStore` to persist your sessions `IMap`, the
* Reloading triggers `EntryAddedListener` results in `SessionCreatedEvent` being re-published
* Reloading uses default TTL for a given `IMap` results in sessions losing their original TTL
[[api-cookieserializer]]
=== Using `CookieSerializer`
A `CookieSerializer` is responsible for defining how the session cookie is written.
Spring Session comes with a default implementation using `DefaultCookieSerializer`.
[[api-cookieserializer-bean]]
==== Exposing `CookieSerializer` as a bean
Exposing the `CookieSerializer` as a Spring bean augments the existing configuration when you use configurations like `@EnableRedisHttpSession`.
The following example shows how to do so:
====
[source,java]
----
include::{samples-dir}spring-session-sample-javaconfig-custom-cookie/src/main/java/sample/Config.java[tags=cookie-serializer]
----
<1> We customize the name of the cookie to be `JSESSIONID`.
<2> We customize the path of the cookie to be `/` (rather than the default of the context root).
<3> We customize the domain name pattern (a regular expression) to be `^.+?\\.(\\w+\\.[a-z]+)$`.
This allows sharing a session across domains and applications.
If the regular expression does not match, no domain is set and the existing domain is used.
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
This means that a request to https://child.example.com sets the domain to `example.com`.
However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and, thus, still works in development without any changes being necessary for production.
====
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
[[api-cookieserializer-customization]]
==== Customizing `CookieSerializer`
You can customize how the session cookie is written by using any of the following configuration options on the `DefaultCookieSerializer`.
* `cookieName`: The name of the cookie to use.
Default: `SESSION`.
* `useSecureCookie`: Specifies whether a secure cookie should be used.
Default: Use the value of `HttpServletRequest.isSecure()` at the time of creation.
* `cookiePath`: The path of the cookie.
Default: The context root.
* `cookieMaxAge`: Specifies the max age of the cookie to be set at the time the session is created.
Default: `-1`, which indicates the cookie should be removed when the browser is closed.
* `jvmRoute`: Specifies a suffix to be appended to the session ID and included in the cookie.
Used to identify which JVM to route to for session affinity.
With some implementations (that is, Redis) this option provides no performance benefit.
However, it can help with tracing logs of a particular user.
* `domainName`: Allows specifying a specific domain name to be used for the cookie.
This option is simple to understand but often requires a different configuration between development and production environments.
See `domainNamePattern` as an alternative.
* `domainNamePattern`: A case-insensitive pattern used to extract the domain name from the `HttpServletRequest#getServerName()`.
The pattern should provide a single grouping that is used to extract the value of the cookie domain.
If the regular expression does not match, no domain is set and the existing domain is used.
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
* `sameSite`: The value for the `SameSite` cookie directive.
To disable the serialization of the `SameSite` cookie directive, you may set this value to `null`.
Default: `Lax`
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
[[custom-sessionrepository]]
== Customing `SessionRepository`
@@ -1351,7 +1422,7 @@ The minimum requirements for Spring Session are:
* Java 8+.
* If you run in a Servlet Container (not required), Servlet 3.1+.
* If you use other Spring libraries (not required), the minimum required version is Spring 5.0.x.
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisoperationssessionrepository-expiration,Session Expiration>>
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisindexedsessionrepository-expiration,Session Expiration>>
* `@EnableHazelcastHttpSession` requires Hazelcast 3.6+. This is necessary to support <<api-enablehazelcasthttpsession-storage,`FindByIndexNameSessionRepository`>>
NOTE: At its core, Spring Session has a required dependency only on `spring-jcl`.

View File

@@ -30,19 +30,18 @@ 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.jdbc.datasource.DataSourceTransactionManager;
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.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
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.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@@ -120,32 +119,31 @@ class IndexDocTests {
@Test
@SuppressWarnings("unused")
void newRedisOperationsSessionRepository() {
// tag::new-redisoperationssessionrepository[]
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisOperationsSessionRepository(redisTemplate);
// end::new-redisoperationssessionrepository[]
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newReactiveRedisOperationsSessionRepository() {
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer()).build();
// tag::new-reactiveredisoperationssessionrepository[]
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisOperationsSessionRepository(
redisTemplate);
// end::new-reactiveredisoperationssessionrepository[]
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
@@ -158,25 +156,25 @@ class IndexDocTests {
@Test
@SuppressWarnings("unused")
void newJdbcOperationsSessionRepository() {
// tag::new-jdbcoperationssessionrepository[]
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure JdbcTemplate ...
// ... configure jdbcTemplate ...
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionManager ...
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcOperationsSessionRepository(jdbcTemplate,
transactionManager);
// end::new-jdbcoperationssessionrepository[]
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newHazelcastSessionRepository() {
// tag::new-hazelcastsessionrepository[]
void newHazelcastIndexedSessionRepository() {
// tag::new-hazelcastindexedsessionrepository[]
Config config = new Config();
@@ -184,8 +182,8 @@ class IndexDocTests {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
HazelcastSessionRepository repository = new HazelcastSessionRepository(hazelcastInstance);
// end::new-hazelcastsessionrepository[]
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
// end::new-hazelcastindexedsessionrepository[]
}
@Test

View File

@@ -48,13 +48,13 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
// tag::configure-redis-action[]
@Bean
public static ConfigureRedisAction configureRedisAction() {
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// end::configure-redis-action[]
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}

View File

@@ -19,12 +19,15 @@ 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.hazelcast.HazelcastSessionRepository;
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;
@@ -35,17 +38,17 @@ public class HazelcastHttpSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
Config config = new Config();
config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config); // <3>
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>
}
}

View File

@@ -32,12 +32,12 @@ class HttpSessionListenerJavaConfigTests extends AbstractHttpSessionListenerTest
static class MockConfig {
@Bean
public static RedisConnectionFactory redisConnectionFactory() {
static RedisConnectionFactory redisConnectionFactory() {
return AbstractHttpSessionListenerTests.createMockRedisConnection();
}
@Bean
public SecuritySessionDestroyedListener securitySessionDestroyedListener() {
SecuritySessionDestroyedListener securitySessionDestroyedListener() {
return new SecuritySessionDestroyedListener();
}

View File

@@ -19,6 +19,7 @@ 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;
@@ -41,14 +42,16 @@ public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapte
protected void configure(HttpSecurity http) throws Exception {
http
// ... additional configuration ...
.rememberMe()
.rememberMeServices(rememberMeServices());
.rememberMe((rememberMe) -> rememberMe
.rememberMeServices(rememberMeServices())
);
// end::http-rememberme[]
http
.formLogin().and()
.authorizeRequests()
.anyRequest().authenticated();
.formLogin(Customizer.withDefaults())
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
);
}
// tag::rememberme-bean[]

View File

@@ -40,9 +40,10 @@ public class SecurityConfiguration<S extends Session> extends WebSecurityConfigu
// @formatter:off
http
// other config goes here...
.sessionManagement()
.sessionManagement((sessionManagement) -> sessionManagement
.maximumSessions(2)
.sessionRegistry(sessionRegistry());
.sessionRegistry(sessionRegistry())
);
// @formatter:on
}

View File

@@ -0,0 +1,48 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
}
}
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.dependency-set'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
dependencies {
compile project(':spring-session-core')
optional "com.hazelcast:hazelcast:4.0.3"
compile "org.springframework:spring-context"
compile "javax.annotation:javax.annotation-api"
testCompile "javax.servlet:javax.servlet-api"
testCompile "org.springframework:spring-web"
testCompile "org.junit.jupiter:junit-jupiter-api"
testCompile "org.springframework.security:spring-security-core"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile "org.testcontainers:testcontainers"
integrationTestCompile "com.hazelcast:hazelcast:4.0.3"
integrationTestCompile project(":spring-session-hazelcast")
}

View File

@@ -0,0 +1,200 @@
/*
* 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.hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.MapSession;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for {@link Hazelcast4IndexedSessionRepository} integration tests.
*
* @author Eleftheria Stein
*/
abstract class AbstractHazelcast4IndexedSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Autowired
private HazelcastInstance hazelcastInstance;
@Autowired
private Hazelcast4IndexedSessionRepository repository;
@Test
void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
assertThat(hazelcastMap.size()).isEqualTo(0);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.size()).isEqualTo(1);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.deleteById(sessionId);
assertThat(hazelcastMap.size()).isEqualTo(0);
}
@Test
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenChangeTwice() {
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
String originalId = toSave.getId();
String changeId1 = toSave.changeSessionId();
String changeId2 = toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(originalId)).isNull();
assertThat(this.repository.findById(changeId1)).isNull();
assertThat(this.repository.findById(changeId2)).isNotNull();
this.repository.deleteById(changeId2);
}
@Test
void changeSessionIdWhenSetAttributeOnChangedSession() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
findById.setAttribute(attrName, attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenHasNotSaved() {
HazelcastSession toSave = this.repository.createSession();
String originalId = toSave.getId();
toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(toSave.getId())).isNotNull();
assertThat(this.repository.findById(originalId)).isNull();
this.repository.deleteById(toSave.getId());
}
@Test // gh-1076
void attemptToUpdateSessionAfterDelete() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.deleteById(sessionId);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNull();
}
@Test
void createAndUpdateSession() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
}
@Test
void createSessionWithSecurityContextAndFindById() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken("saves-" + System.currentTimeMillis(),
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.hazelcast;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using client-server
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class ClientServerHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:4.0.2")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml");
@BeforeAll
static void setUpClass() {
container.start();
}
@AfterAll
static void tearDownClass() {
container.stop();
}
@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig()
.addAddress(container.getContainerIpAddress() + ":" + container.getFirstMappedPort());
clientConfig.getUserCodeDeploymentConfig().setEnabled(true).addClass(Session.class)
.addClass(MapSession.class).addClass(Hazelcast4SessionUpdateEntryProcessor.class);
return HazelcastClient.newHazelcastClient(clientConfig);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using embedded
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class EmbeddedHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.hazelcast;
import com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.IndexConfig;
import com.hazelcast.config.IndexType;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.MapSession;
/**
* Utility class for Hazelcast integration tests.
*
* @author Eleftheria Stein
*/
final class Hazelcast4ITestUtils {
private Hazelcast4ITestUtils() {
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @return the Hazelcast instance
*/
static HazelcastInstance embeddedHazelcastServer() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
AttributeConfig attributeConfig = new AttributeConfig()
.setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);
return Hazelcast.newHazelcastInstance(config);
}
}

View File

@@ -0,0 +1,218 @@
/*
* 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.hazelcast;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
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;
/**
* Ensure that the appropriate SessionEvents are fired at the expected times. Additionally
* ensure that the interactions with the {@link SessionRepository} abstraction behave as
* expected after each SessionEvent.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SessionEventHazelcast4IndexedSessionRepositoryTests<S extends Session> {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
@Autowired
private SessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@BeforeEach
void setup() {
this.registry.clear();
}
@Test
void saveSessionTest() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
S sessionToSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
Session session = this.repository.findById(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(sessionToSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(sessionToSave.getAttribute(expectedAttributeName));
}
@Test
void expiredSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
assertThat(sessionToSave.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void deletedSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
this.repository.deleteById(sessionToSave.getId());
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionDeletedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionDeletedEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void saveUpdatesTimeToLiveTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofSeconds(3));
this.repository.save(sessionToSave);
Thread.sleep(2000);
// Get and save the session like SessionRepositoryFilter would.
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
Thread.sleep(2000);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNotNull();
}
@Test // gh-1077
void changeSessionIdNoEventTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
sessionToSave.changeSessionId();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isFalse();
}
@Test // gh-1300
void updateMaxInactiveIntervalTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
sessionToUpdate.setMaxInactiveInterval(Duration.ofSeconds(1));
this.repository.save(sessionToUpdate);
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {
@Bean
HazelcastInstance embeddedHazelcast() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
@Bean
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

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