Support querying for sessions by user identifier
Fixes gh-7
This commit is contained in:
172
docs/src/docs/asciidoc/guides/findbyusername.adoc
Normal file
172
docs/src/docs/asciidoc/guides/findbyusername.adoc
Normal file
@@ -0,0 +1,172 @@
|
||||
= Spring Session - find by username
|
||||
Rob Winch
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to find sessions by username.
|
||||
|
||||
NOTE: The completed guide can be found in the <<findbyusername-sample, findbyusername application>>.
|
||||
|
||||
[[findbyusername-assumptions]]
|
||||
== Assumptions
|
||||
|
||||
The guide assumes you have already added Spring Session using the built in Redis configuration support to your application.
|
||||
The guide also assumes you have already applied Spring Security to your application.
|
||||
However, we the guide will be somewhat general purpose and can be applied to any technology with minimal changes we will discuss.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If you need to learn how to add Spring Session to your project, please refer to the listing of link:../#samples[samples and guides]
|
||||
====
|
||||
|
||||
== About the Sample
|
||||
|
||||
Our sample is using this feature to invalidate the users session that might have been compromised.
|
||||
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, etc.
|
||||
|
||||
Wouldn't it be nice if we could allow the user to invalidate the session at the library from any device they authenticate with?
|
||||
This sample demonstrates how this is possible.
|
||||
|
||||
[[findbyusernamesessionrepository]]
|
||||
== FindByUsernameSessionRepository
|
||||
|
||||
In order to look up a user by their username, you must first choose a `SessionRepository` that implements <<index.doc#ap-findbyusernamesessionrepository,FindByUsernameSessionRepository>>.
|
||||
Our sample application assumes that the Redis support is already setup, so we are ready to go.
|
||||
|
||||
== Mapping the username
|
||||
|
||||
`FindByUsernameSessionRepository` can only find a session by the username, if the developer instructs Spring Session what user is associated with the `Session`.
|
||||
This is done by ensuring that the session attribute with the name `Session.FindByUsernameSessionRepository` is populated with the username.
|
||||
|
||||
Generally, speaking this can be done with the following code immediately after the user authenticates:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/session/SpringSessionPrincipalNameSuccessHandler.java[tags=set-username]
|
||||
----
|
||||
|
||||
== Mapping the username with Spring Security
|
||||
|
||||
Since we are using Spring Security, it makes perfect sense to provide a custom `AuthenticationSuccessHandler` that populates the username.
|
||||
For example:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
We plan to provide first class integration with Spring Security to make this process easier in the future.
|
||||
For details track https://github.com/spring-projects/spring-session/issues/266[gh-266].
|
||||
====
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/session/SpringSessionPrincipalNameSuccessHandler.java[tags=class]
|
||||
----
|
||||
|
||||
In order to support multiple handlers, we need to also create a custom `AuthenticationSuccessHandler` that delegates to multiple `AuthenticationSuccessHandler` instances.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/session/CompositeAuthenticationSuccessHandler.java[tags=class]
|
||||
----
|
||||
|
||||
Next we need to provide a custom `AuthenticationSuccessHandler` with Spring Security.
|
||||
In Java configuration, we can do this using the following:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/config/SecurityConfig.java[tags=handler]
|
||||
----
|
||||
|
||||
We can then configure authentication success with the following:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/config/SecurityConfig.java[tags=config]
|
||||
----
|
||||
|
||||
== Adding Additional Data to Session
|
||||
|
||||
It may be nice to associate additional information (i.e. IP Address, the browser, location, etc) to the session.
|
||||
This makes it easier for the user to know which session they are looking at.
|
||||
|
||||
To do this simply determine which session attribute you want to use and what information you wish to provide.
|
||||
Then create a Java bean that is added as a session attribute.
|
||||
For example, our sample application includes the location and access type of the session
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/session/SessionDetails.java[tags=class]
|
||||
----
|
||||
|
||||
We then inject that information into the session on each HTTP request using a `SessionDetailsFilter`.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/session/SessionDetailsFilter.java[tags=dofilterinternal]
|
||||
----
|
||||
|
||||
We obtain the information we want and then set the `SessionDetails` as an attribute in the `Session`.
|
||||
When we retrieve the `Session` by username, we can then use the session to access our `SessionDetails` just like any other session attribute.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
You might be wondering at this point why Spring Session does not provide `SessionDetails` functionality out of the box.
|
||||
The reason, is twofold.
|
||||
The first is that it is very trivial for applications to implement this themselves.
|
||||
The second reason is that the information that is populated in the session (and how frequently that information is updated) is highly application dependent.
|
||||
====
|
||||
|
||||
== Finding sessions for a specific user
|
||||
|
||||
We can now find all the sessions for a specific user.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}findbyusername/src/main/java/sample/mvc/IndexController.java[tags=findbyusername]
|
||||
----
|
||||
|
||||
In our instance, we find all sessions for the currently logged in user.
|
||||
However, this could easily be modified for an administrator to use a form to specify which user to look up.
|
||||
|
||||
[[findbyusername-sample]]
|
||||
== findbyusername Sample Application
|
||||
|
||||
=== Running the findbyusername Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `JedisConnectionFactory` to point to a Redis server.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :samples:findbyusername:tomcatRun
|
||||
----
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
You should also see a listing of active sessions for the currently logged in user.
|
||||
|
||||
Let's emulate the flow we discussed in the <<About the Sample>> section
|
||||
|
||||
* Open a new incognito window and navigate to http://localhost:8080/
|
||||
* Enter the following to log in:
|
||||
** **Username** _user_
|
||||
** **Password** _password_
|
||||
* Terminate your original session
|
||||
* Refresh the original window and see you are logged out
|
||||
@@ -51,6 +51,10 @@ If you are looking to get started with Spring Session, the best place to start i
|
||||
| Demonstrates how to use Spring Session in a REST application to support authenticating with a header.
|
||||
| link:guides/rest.html[REST Guide]
|
||||
|
||||
| {gh-samples-url}findbyusername[Find by Username]
|
||||
| Demonstrates how to use Spring Session to find sessions by username.
|
||||
| link:guides/findbyusername.html[Find by Username]
|
||||
|
||||
| {gh-samples-url}users[Multiple Users]
|
||||
| Demonstrates how to use Spring Session to manage multiple simultaneous browser sessions (i.e Google Accounts).
|
||||
| link:guides/users.html[Manage Multiple Users Guide]
|
||||
@@ -304,6 +308,19 @@ A `SessionRepository` is in charge of creating, retrieving, and persisting `Sess
|
||||
If possible, developers should not interact directly with a `SessionRepository` or a `Session`.
|
||||
Instead, developers should prefer interacting with `SessionRepository` and `Session` indirectly through the <<httpsession,HttpSession>> and <<websocket,WebSocket>> integration.
|
||||
|
||||
[[api-findbyusernamesessionrepository]]
|
||||
== FindByUsernameSessionRepository
|
||||
|
||||
Spring Session's most basic API for using a `Session` is the `SessionRepository`.
|
||||
This API is intentionally very simple, so that it is easy to provide additional implementations with basic functionality.
|
||||
|
||||
The `FindByUsernameSessionRepository` adds a single method to look up all the sessions for a particular user.
|
||||
This is done by ensuring that the session attribute with the name `Session.PRINCIPAL_NAME_ATTRIBUTE_NAME` is populated with the username.
|
||||
It is the responsibility of the developer to ensure the attribute is populated since Spring Session is not aware of the authentication mechanism being used.
|
||||
|
||||
Some `SessionRepository` implementations may choose to implement `FindByUsernameSessionRepository` also.
|
||||
For example, Spring's Redis support implements `FindByUsernameSessionRepository`.
|
||||
|
||||
[[api-redisoperationssessionrepository]]
|
||||
=== RedisOperationsSessionRepository
|
||||
|
||||
|
||||
4
samples/findbyusername/README.adoc
Normal file
4
samples/findbyusername/README.adoc
Normal file
@@ -0,0 +1,4 @@
|
||||
Demonstrates using Spring Session to lookup a user's session by the username.
|
||||
The sample provides a hook to add the current username to the session (required for finding the user) by providing a custom implementation of Spring Security's `AuthenticationSuccessHandler`.
|
||||
|
||||
NOTE: This product includes GeoLite2 data created by MaxMind, available from http://www.maxmind.com
|
||||
54
samples/findbyusername/build.gradle
Normal file
54
samples/findbyusername/build.gradle
Normal file
@@ -0,0 +1,54 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion")
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
apply from: JAVA_GRADLE
|
||||
|
||||
tasks.findByPath("artifactoryPublish")?.enabled = false
|
||||
|
||||
group = 'samples'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-session'),
|
||||
"org.springframework.boot:spring-boot-starter-redis",
|
||||
"org.springframework.boot:spring-boot-starter-web",
|
||||
"org.springframework.boot:spring-boot-starter-thymeleaf",
|
||||
"nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect",
|
||||
"org.springframework.security:spring-security-web:$springSecurityVersion",
|
||||
"org.springframework.security:spring-security-config:$springSecurityVersion",
|
||||
"com.maxmind.geoip2:geoip2:2.3.1",
|
||||
"org.apache.httpcomponents:httpclient:4.4.1"
|
||||
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test",
|
||||
'org.easytesting:fest-assert:1.4'
|
||||
|
||||
integrationTestCompile gebDependencies,
|
||||
"org.spockframework:spock-spring:$spockVersion"
|
||||
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
doFirst {
|
||||
def port = reservePort()
|
||||
|
||||
def host = 'localhost:' + port
|
||||
systemProperties['geb.build.baseUrl'] = 'http://'+host+'/'
|
||||
systemProperties['geb.build.reportsDir'] = 'build/geb-reports'
|
||||
systemProperties['server.port'] = port
|
||||
systemProperties['management.port'] = 0
|
||||
}
|
||||
}
|
||||
|
||||
def reservePort() {
|
||||
def socket = new ServerSocket(0)
|
||||
def result = socket.localPort
|
||||
socket.close()
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample
|
||||
|
||||
import geb.Browser
|
||||
import geb.spock.*
|
||||
|
||||
import org.openqa.selenium.htmlunit.HtmlUnitDriver
|
||||
import org.springframework.boot.test.IntegrationTest
|
||||
import org.springframework.boot.test.SpringApplicationContextLoader
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.context.web.WebAppConfiguration
|
||||
|
||||
import pages.*
|
||||
import sample.pages.HomePage
|
||||
import sample.pages.LoginPage
|
||||
import spock.lang.Stepwise
|
||||
|
||||
/**
|
||||
* Tests findbyusername web application
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Stepwise
|
||||
@ContextConfiguration(classes = FindByUsernameApplication, loader = SpringApplicationContextLoader)
|
||||
@WebAppConfiguration
|
||||
@IntegrationTest
|
||||
class FindByUsernameTests extends MultiBrowserGebSpec {
|
||||
def 'Unauthenticated user sent to log in page'() {
|
||||
given: 'unauthenticated user request protected page'
|
||||
withBrowserSession a, {
|
||||
via HomePage
|
||||
then: 'sent to the log in page'
|
||||
at LoginPage
|
||||
}
|
||||
}
|
||||
|
||||
def 'Log in views home page'() {
|
||||
when: 'log in successfully'
|
||||
a.login()
|
||||
then: 'sent to original page'
|
||||
a.at HomePage
|
||||
and: 'the username is displayed'
|
||||
a.username == 'user'
|
||||
and: 'Spring Session Management is being used'
|
||||
a.driver.manage().cookies.find { it.name == 'SESSION' }
|
||||
and: 'Standard Session is NOT being used'
|
||||
!a.driver.manage().cookies.find { it.name == 'JSESSIONID' }
|
||||
and: 'Session id exists and terminate disabled'
|
||||
a.sessionId
|
||||
a.terminate(a.sessionId).@disabled
|
||||
}
|
||||
|
||||
def 'Other Browser: Unauthenticated user sent to log in page'() {
|
||||
given:
|
||||
withBrowserSession b, {
|
||||
via HomePage
|
||||
then: 'sent to the log in page'
|
||||
at LoginPage
|
||||
}
|
||||
}
|
||||
|
||||
def 'Other Browser: Log in views home page'() {
|
||||
when: 'log in successfully'
|
||||
b.login()
|
||||
then: 'sent to original page'
|
||||
b.at HomePage
|
||||
and: 'the username is displayed'
|
||||
b.username == 'user'
|
||||
and: 'Spring Session Management is being used'
|
||||
b.driver.manage().cookies.find { it.name == 'SESSION' }
|
||||
and: 'Standard Session is NOT being used'
|
||||
!b.driver.manage().cookies.find { it.name == 'JSESSIONID' }
|
||||
and: 'Session id exists and terminate disabled'
|
||||
b.sessionId
|
||||
b.terminate(b.sessionId).@disabled
|
||||
}
|
||||
|
||||
def 'Other Browser: Terminate Original'() {
|
||||
setup: 'get the session id from browser a'
|
||||
def sessionId = a.sessionId
|
||||
when: 'we terminate browser a session'
|
||||
b.terminate(sessionId).click(HomePage)
|
||||
then: 'session is not listed'
|
||||
!b.terminate(sessionId)
|
||||
when: 'browser a visits home page after session was terminated'
|
||||
a.via HomePage
|
||||
then: 'browser a sent to log in'
|
||||
a.at LoginPage
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample;
|
||||
import geb.*
|
||||
import spock.lang.*
|
||||
|
||||
/**
|
||||
* https://github.com/kensiprell/geb-multibrowser
|
||||
*/
|
||||
abstract class MultiBrowserGebSpec extends Specification {
|
||||
String gebConfEnv = null
|
||||
String gebConfScript = null
|
||||
|
||||
// Map of geb browsers which can be referenced by name in the spec
|
||||
// THese currently share the same config. This is not a problem for
|
||||
// my uses, but I can see potential for wanting to configure different
|
||||
// browsers separately
|
||||
@Shared _browsers = createBrowserMap()
|
||||
def currentBrowser
|
||||
|
||||
def createBrowserMap() {
|
||||
[:].withDefault { new Browser(createConf()) }
|
||||
}
|
||||
|
||||
Configuration createConf() {
|
||||
// Use the standard configured geb driver, but turn off cacheing so
|
||||
// we can run multiple
|
||||
def conf = new ConfigurationLoader(gebConfEnv).getConf(gebConfScript)
|
||||
conf.cacheDriver = false
|
||||
return conf
|
||||
}
|
||||
|
||||
def withBrowserSession(browser, Closure c) {
|
||||
currentBrowser = browser
|
||||
def returnedValue = c.call()
|
||||
currentBrowser = null
|
||||
returnedValue
|
||||
}
|
||||
|
||||
void resetBrowsers() {
|
||||
_browsers.each { k, browser ->
|
||||
if (browser.config?.autoClearCookies) {
|
||||
browser.clearCookiesQuietly()
|
||||
}
|
||||
browser.quit()
|
||||
}
|
||||
_browsers = createBrowserMap()
|
||||
}
|
||||
|
||||
def propertyMissing(String name) {
|
||||
if(currentBrowser) {
|
||||
return currentBrowser."$name"
|
||||
} else {
|
||||
return _browsers[name]
|
||||
}
|
||||
}
|
||||
|
||||
def methodMissing(String name, args) {
|
||||
if(currentBrowser) {
|
||||
return currentBrowser."$name"(*args)
|
||||
} else {
|
||||
def browser = _browsers[name]
|
||||
if(args) {
|
||||
return browser."${args[0]}"(*(args[1..-1]))
|
||||
} else {
|
||||
return browser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def propertyMissing(String name, value) {
|
||||
if(!currentBrowser) throw new IllegalArgumentException("No context for setting property $name")
|
||||
currentBrowser."$name" = value
|
||||
}
|
||||
|
||||
private isSpecStepwise() {
|
||||
this.class.getAnnotation(Stepwise) != null
|
||||
}
|
||||
|
||||
def cleanup() {
|
||||
if (!isSpecStepwise()) resetBrowsers()
|
||||
}
|
||||
|
||||
def cleanupSpec() {
|
||||
if (isSpecStepwise()) resetBrowsers()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.pages
|
||||
|
||||
import geb.*
|
||||
|
||||
/**
|
||||
* The home page
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class HomePage extends Page {
|
||||
static url = ''
|
||||
static at = { assert driver.title == 'Spring Session Sample - Secured Content'; true}
|
||||
static content = {
|
||||
username { $('#un').text() }
|
||||
logout(to:LoginPage) { $('input[type=submit]').click() }
|
||||
sessions { moduleList AttributeRow, $("table tr").tail() }
|
||||
terminate(required:false) { sessionId -> $("#terminate-$sessionId") }
|
||||
sessionId { $("#session-id").text() }
|
||||
}
|
||||
}
|
||||
|
||||
class AttributeRow extends Module {
|
||||
static content = {
|
||||
cell { $("td", it) }
|
||||
location { cell(0).text() }
|
||||
created { cell(1).text() }
|
||||
lastUpdated { cell(2).text() }
|
||||
information { cell(3).text() }
|
||||
terminate { cell(4).text() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.pages
|
||||
|
||||
import geb.*
|
||||
|
||||
/**
|
||||
* The Links Page
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class LoginPage extends Page {
|
||||
static url = '/login'
|
||||
static at = { assert driver.title == 'Spring Session Sample - Log In'; true}
|
||||
static content = {
|
||||
form { $('form') }
|
||||
submit { $('button[type=submit]') }
|
||||
login(required:false) { user='user', pass='password' ->
|
||||
form.username = user
|
||||
form.password = pass
|
||||
submit.click(HomePage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableAutoConfiguration
|
||||
public class FindByUsernameApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FindByUsernameApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class GeoConfig {
|
||||
|
||||
@Bean
|
||||
public DatabaseReader geoDatabaseReader(@Value("classpath:GeoLite2-City.mmdb") InputStream geoInputStream) throws Exception {
|
||||
return new DatabaseReader.Builder(geoInputStream).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
// tag::class[]
|
||||
@EnableRedisHttpSession // <1>
|
||||
public class HttpSessionConfig { }
|
||||
// end::class[]
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
|
||||
import sample.session.CompositeAuthenticationSuccessHandler;
|
||||
import sample.session.SpringSessionPrincipalNameSuccessHandler;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
// tag::config[]
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
CompositeAuthenticationSuccessHandler successHandler = createHandler();
|
||||
|
||||
http
|
||||
.formLogin()
|
||||
.successHandler(successHandler)
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/resources/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.logout()
|
||||
.permitAll();
|
||||
}
|
||||
// end::config[]
|
||||
|
||||
// tag::handler[]
|
||||
private CompositeAuthenticationSuccessHandler createHandler() {
|
||||
SpringSessionPrincipalNameSuccessHandler setUsernameHandler =
|
||||
new SpringSessionPrincipalNameSuccessHandler();
|
||||
SavedRequestAwareAuthenticationSuccessHandler defaultHandler =
|
||||
new SavedRequestAwareAuthenticationSuccessHandler();
|
||||
|
||||
CompositeAuthenticationSuccessHandler successHandler =
|
||||
new CompositeAuthenticationSuccessHandler(setUsernameHandler, defaultHandler);
|
||||
return successHandler;
|
||||
}
|
||||
// end::handler[]
|
||||
|
||||
@Autowired
|
||||
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth
|
||||
.inMemoryAuthentication()
|
||||
.withUser("user").password("password").roles("USER");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.mvc;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.FindByPrincipalNameSessionRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
/**
|
||||
* Controller for sending the user to the login view.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
public class IndexController {
|
||||
// tag::findbyusername[]
|
||||
@Autowired
|
||||
FindByPrincipalNameSessionRepository<? extends ExpiringSession> sessions;
|
||||
|
||||
@RequestMapping("/")
|
||||
public String index(Principal principal, Model model) {
|
||||
Collection<? extends ExpiringSession> usersSessions =
|
||||
sessions.findByPrincipalName(principal.getName()).values();
|
||||
model.addAttribute("sessions", usersSessions);
|
||||
return "index";
|
||||
}
|
||||
// end::findbyusername[]
|
||||
|
||||
@RequestMapping(value = "/sessions/{sessionIdToDelete}", method = RequestMethod.DELETE)
|
||||
public String removeSession(Principal principal, @PathVariable String sessionIdToDelete) {
|
||||
Set<String> usersSessionIds = sessions.findByPrincipalName(principal.getName()).keySet();
|
||||
if(usersSessionIds.contains(sessionIdToDelete)) {
|
||||
sessions.delete(sessionIdToDelete);
|
||||
}
|
||||
|
||||
return "redirect:/";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.mvc;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* Returns view for log in page
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Controller
|
||||
public class LoginController {
|
||||
|
||||
@RequestMapping("/login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.session;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
// tag::class[]
|
||||
public class CompositeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
private List<AuthenticationSuccessHandler> handlers;
|
||||
|
||||
public CompositeAuthenticationSuccessHandler(AuthenticationSuccessHandler... handlers) {
|
||||
super();
|
||||
this.handlers = Arrays.asList(handlers);
|
||||
}
|
||||
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException, ServletException {
|
||||
for(AuthenticationSuccessHandler handler : handlers) {
|
||||
handler.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::class[]
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.session;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An example of how users can provide details about their session.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see SessionDetailsFilter
|
||||
*/
|
||||
// tag::class[]
|
||||
public class SessionDetails implements Serializable {
|
||||
private String location;
|
||||
|
||||
private String accessType;
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getAccessType() {
|
||||
return accessType;
|
||||
}
|
||||
|
||||
public void setAccessType(String accessType) {
|
||||
this.accessType = accessType;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 8850489178248613501L;
|
||||
}
|
||||
// end::class[]
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.session;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
|
||||
/**
|
||||
* Inserts the session details into the session for every request. Some users
|
||||
* may prefer to insert session details only after authentication. This is fine,
|
||||
* but it may be valuable to the most up to date information so that if someone
|
||||
* stole the user's session id it can be observed.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
// tag::class[]
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 101)
|
||||
public class SessionDetailsFilter extends OncePerRequestFilter {
|
||||
static final String UNKNOWN = "Unknown";
|
||||
|
||||
private DatabaseReader reader;
|
||||
|
||||
@Autowired
|
||||
public SessionDetailsFilter(DatabaseReader reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
// tag::dofilterinternal[]
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
chain.doFilter(request, response);
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
String remoteAddr = getRemoteAddress(request);
|
||||
String geoLocation = getGeoLocation(remoteAddr);
|
||||
|
||||
SessionDetails details = new SessionDetails();
|
||||
details.setAccessType(request.getHeader("User-Agent"));
|
||||
details.setLocation(remoteAddr + " " + geoLocation);
|
||||
|
||||
session.setAttribute("SESSION_DETAILS", details);
|
||||
}
|
||||
}
|
||||
// end::dofilterinternal[]
|
||||
|
||||
String getGeoLocation(String remoteAddr) {
|
||||
try {
|
||||
CityResponse city = reader.city(InetAddress.getByName(remoteAddr));
|
||||
String cityName = city.getCity().getName();
|
||||
String countryName = city.getCountry().getName();
|
||||
if(cityName == null && countryName == null) {
|
||||
return null;
|
||||
} else if(cityName == null) {
|
||||
return countryName;
|
||||
} else if(countryName == null) {
|
||||
return cityName;
|
||||
}
|
||||
return cityName + ", " + countryName;
|
||||
} catch (Exception e) {
|
||||
return UNKNOWN;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private String getRemoteAddress(HttpServletRequest request) {
|
||||
String remoteAddr = request.getHeader("X-FORWARDED-FOR");
|
||||
if (remoteAddr == null) {
|
||||
remoteAddr = request.getRemoteAddr();
|
||||
} else if (remoteAddr.contains(",")) {
|
||||
remoteAddr = remoteAddr.split(",")[0];
|
||||
}
|
||||
return remoteAddr;
|
||||
}
|
||||
}
|
||||
//end::class[]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.session;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.session.FindByPrincipalNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
|
||||
/**
|
||||
* Inserts the username into Spring session after we successfully authenticate.
|
||||
* Adding the principal name to the session is a requirement of
|
||||
* {@link FindByPrincipalNameSessionRepository}
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
// tag::class[]
|
||||
public class SpringSessionPrincipalNameSuccessHandler
|
||||
implements AuthenticationSuccessHandler {
|
||||
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException, ServletException {
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
String currentUsername = authentication.getName();
|
||||
|
||||
// tag::set-username[]
|
||||
session.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, currentUsername);
|
||||
// end::set-username[]
|
||||
}
|
||||
}
|
||||
// end::class[]
|
||||
BIN
samples/findbyusername/src/main/resources/GeoLite2-City.mmdb
Normal file
BIN
samples/findbyusername/src/main/resources/GeoLite2-City.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 MiB |
@@ -0,0 +1,2 @@
|
||||
spring.thymeleaf.cache=false
|
||||
spring.template.cache=false
|
||||
1092
samples/findbyusername/src/main/resources/static/resources/css/bootstrap-responsive.css
vendored
Normal file
1092
samples/findbyusername/src/main/resources/static/resources/css/bootstrap-responsive.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6039
samples/findbyusername/src/main/resources/static/resources/css/bootstrap.css
vendored
Normal file
6039
samples/findbyusername/src/main/resources/static/resources/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,36 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
|
||||
<head>
|
||||
<title>Secured Content</title>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<h1>Secured Page</h1>
|
||||
<p>This page is secured using Spring Boot, Spring Session, and Spring Security.</p>
|
||||
|
||||
<p>Your current session id is <span id="session-id" th:text="${#httpSession.id}"></span></p>
|
||||
|
||||
<table class="table table-stripped">
|
||||
<tr>
|
||||
<th>Id Suffix</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Information</th>
|
||||
<th>Terminate</th>
|
||||
</tr>
|
||||
<tr th:each="session : ${sessions}" th:with="details=${session.getAttribute('SESSION_DETAILS')}">
|
||||
<td th:text="${session.id.substring(30)}"></td>
|
||||
<td th:text="${details.location}"></td>
|
||||
<td th:text="${#dates.format(new java.util.Date(session.creationTime),'dd/MMM/yyyy HH:mm:ss')}"></td>
|
||||
<td th:text="${#dates.format(new java.util.Date(session.lastAccessedTime),'dd/MMM/yyyy HH:mm:ss')}"></td>
|
||||
<td th:text="${details.accessType}"></td>
|
||||
<td>
|
||||
<form th:action="@{'/sessions/' + ${session.id}}" th:method="delete">
|
||||
<input th:id="'terminate-' + ${session.id}" type="submit" value="Terminate" th:disabled="${session.id == #httpSession.id}"/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
122
samples/findbyusername/src/main/resources/templates/layout.html
Normal file
122
samples/findbyusername/src/main/resources/templates/layout.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
|
||||
<link rel="icon" type="image/x-icon" th:href="@{/resources/img/favicon.ico}" href="../static/img/favicon.ico"/>
|
||||
<link th:href="@{/resources/css/bootstrap.css}" href="../static/css/bootstrap.css" rel="stylesheet"></link>
|
||||
<style type="text/css">
|
||||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
/* The html and body elements cannot have any padding or margin. */
|
||||
}
|
||||
|
||||
/* Wrapper for page content to push down footer */
|
||||
#wrap {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
/* Negative indent footer by it's height */
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
|
||||
/* Set the fixed height of the footer here */
|
||||
#push,
|
||||
#footer {
|
||||
height: 60px;
|
||||
}
|
||||
#footer {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Lastly, apply responsive CSS fixes as necessary */
|
||||
@media (max-width: 767px) {
|
||||
#footer {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Custom page CSS
|
||||
-------------------------------------------------- */
|
||||
/* Not required for template or sticky footer method. */
|
||||
|
||||
.container {
|
||||
width: auto;
|
||||
max-width: 680px;
|
||||
}
|
||||
.container .credit {
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
a {
|
||||
color: green;
|
||||
}
|
||||
.navbar-form {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
<link th:href="@{resources/css/bootstrap-responsive.css}" href="/static/css/bootstrap-responsive.css" rel="stylesheet"></link>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div class="navbar navbar-inverse navbar-static-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="brand" th:href="@{/}"><img th:src="@{/resources/img/logo.png}" alt="Spring Security Sample"/></a>
|
||||
|
||||
<div class="nav-collapse collapse"
|
||||
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
|
||||
<div th:if="${currentUser != null}">
|
||||
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
|
||||
<input type="submit" value="Log out" />
|
||||
</form>
|
||||
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
|
||||
sample_user
|
||||
</p>
|
||||
</div>
|
||||
<ul class="nav">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Begin page content -->
|
||||
<div class="container">
|
||||
<div class="alert alert-success"
|
||||
th:if="${globalMessage}"
|
||||
th:text="${globalMessage}">
|
||||
Some Success message
|
||||
</div>
|
||||
<div layout:fragment="content">
|
||||
Fake content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="push"><!-- --></div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<div class="container">
|
||||
<p class="muted credit">This product includes GeoLite2 data created by MaxMind, available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorator="layout">
|
||||
<head>
|
||||
<title>Log In</title>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<form name="f" th:action="@{/login}" method="post">
|
||||
<fieldset>
|
||||
<legend>Please Login</legend>
|
||||
<div th:if="${param.error}" class="alert alert-error">Invalid
|
||||
username and password.</div>
|
||||
<div th:if="${param.logout}" class="alert alert-success">You
|
||||
have been logged out.</div>
|
||||
<label for="username">Username</label> <input type="text"
|
||||
id="username" name="username" /> <label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" />
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn">Log in</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<div class="container">
|
||||
<p>The intention is to demo a way to terminate a user's active
|
||||
session without access to the device. Consider the following</p>
|
||||
<ul>
|
||||
<li>User goes to library and authenticates to the application</li>
|
||||
<li>User goes home and realizes they forgot to log out</li>
|
||||
<li>User can log in and terminate the session from the library
|
||||
using clues like the location, created time, last accessed time,
|
||||
etc.</li>
|
||||
</ul>
|
||||
|
||||
<p>To emulate the workflow:</p>
|
||||
|
||||
<ul>
|
||||
<li>Sign in with the <b>username</b> "user" and <b>password</b> "password"</li>
|
||||
<li>Sign in again using an incognito window</li>
|
||||
<li>Terminate your original session</li>
|
||||
<li>Refresh the original window and see you are logged out</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.session;
|
||||
|
||||
import static org.fest.assertions.Assertions.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
|
||||
import sample.config.GeoConfig;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = GeoConfig.class)
|
||||
public class SessionDetailsFilterTests {
|
||||
@Autowired
|
||||
DatabaseReader reader;
|
||||
|
||||
SessionDetailsFilter filter;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
filter = new SessionDetailsFilter(reader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGeoLocationHanldesInvalidIp() {
|
||||
assertThat(filter.getGeoLocation("a")).isEqualTo(SessionDetailsFilter.UNKNOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGeoLocationNullCity() {
|
||||
assertThat(filter.getGeoLocation("22.231.113.64")).isEqualTo("United States");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGeoLocationBoth() {
|
||||
assertThat(filter.getGeoLocation("184.154.83.119")).isEqualTo("Chicago, United States");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ rootProject.name = 'spring-session-build'
|
||||
include 'docs'
|
||||
|
||||
include 'samples:boot'
|
||||
include 'samples:findbyusername'
|
||||
include 'samples:hazelcast'
|
||||
include 'samples:httpsession'
|
||||
include 'samples:httpsession-xml'
|
||||
|
||||
@@ -17,6 +17,9 @@ package org.springframework.session.data.redis;
|
||||
|
||||
import static org.fest.assertions.Assertions.assertThat;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -25,11 +28,13 @@ import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
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.FindByPrincipalNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
@@ -45,27 +50,36 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@WebAppConfiguration
|
||||
public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
@Autowired
|
||||
private SessionRepository<S> repository;
|
||||
private FindByPrincipalNameSessionRepository<S> repository;
|
||||
|
||||
@Autowired
|
||||
private SessionEventRegistry registry;
|
||||
|
||||
@Autowired
|
||||
RedisOperations<Object, Object> redis;
|
||||
|
||||
@Test
|
||||
public void saves() throws InterruptedException {
|
||||
String username = "saves-"+System.currentTimeMillis();
|
||||
|
||||
String usernameSessionKey = RedisOperationsSessionRepository.PRINCIPAL_NAME_PREFIX + username;
|
||||
|
||||
S toSave = repository.createSession();
|
||||
String expectedAttributeName = "a";
|
||||
String expectedAttributeValue = "b";
|
||||
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
|
||||
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username,"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
|
||||
toSaveContext.setAuthentication(toSaveToken);
|
||||
toSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
|
||||
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, username);
|
||||
registry.clear();
|
||||
|
||||
repository.save(toSave);
|
||||
|
||||
assertThat(registry.receivedEvent()).isTrue();
|
||||
assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class);
|
||||
assertThat(redis.boundSetOps(usernameSessionKey).members()).contains(toSave.getId());
|
||||
|
||||
Session session = repository.getSession(toSave.getId());
|
||||
|
||||
@@ -79,6 +93,7 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
|
||||
assertThat(repository.getSession(toSave.getId())).isNull();
|
||||
assertThat(registry.getEvent()).isInstanceOf(SessionDestroyedEvent.class);
|
||||
assertThat(redis.boundSetOps(usernameSessionKey).members()).excludes(toSave.getId());
|
||||
|
||||
|
||||
assertThat(registry.getEvent().getSession().getAttribute(expectedAttributeName)).isEqualTo(expectedAttributeValue);
|
||||
@@ -103,6 +118,28 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
assertThat(session.getAttribute("1")).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByPrincipalName() throws Exception {
|
||||
String principalName = "findByPrincipalName" + UUID.randomUUID();
|
||||
S toSave = repository.createSession();
|
||||
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
|
||||
|
||||
repository.save(toSave);
|
||||
|
||||
Map<String, S> findByPrincipalName = repository.findByPrincipalName(principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
|
||||
repository.delete(toSave.getId());
|
||||
registry.receivedEvent();
|
||||
|
||||
findByPrincipalName = repository.findByPrincipalName(principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(0);
|
||||
assertThat(findByPrincipalName.keySet()).excludes(toSave.getId());
|
||||
}
|
||||
|
||||
static class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
|
||||
private AbstractSessionEvent event;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extends a basic {@link SessionRepository} to allow finding a session id by
|
||||
* the principal name. The principal name is defined by the {@link Session}
|
||||
* attribute with the name {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
* @param <S>
|
||||
* the type of Session being managed by this
|
||||
* {@link FindByPrincipalNameSessionRepository}
|
||||
*/
|
||||
public interface FindByPrincipalNameSessionRepository<S extends Session> extends SessionRepository<S> {
|
||||
|
||||
/**
|
||||
* Find a Map of the session id to the {@link Session} of all sessions that
|
||||
* contain the session attribute with the name
|
||||
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of the
|
||||
* specified principal name.
|
||||
*
|
||||
* @param principalName
|
||||
* the principal name (i.e. username) to search for
|
||||
* @return a Map (never null) of the session id to the {@link Session} of
|
||||
* all sessions that contain the session attribute with the name
|
||||
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of
|
||||
* the specified principal name. If no results are found, an empty
|
||||
* Map is returned.
|
||||
*/
|
||||
Map<String, S> findByPrincipalName(String principalName);
|
||||
}
|
||||
@@ -26,6 +26,22 @@ import java.util.Set;
|
||||
*/
|
||||
public interface Session {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A common session attribute that contains the current principal name (i.e.
|
||||
* username).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It is the responsibility of the developer to ensure the attribute
|
||||
* is populated since Spring Session is not aware of the authentication
|
||||
* mechanism being used.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.1
|
||||
*/
|
||||
String PRINCIPAL_NAME_ATTRIBUTE_NAME = Session.class.getName().concat(".PRINCIPAL_NAME_ATTRIBUTE_NAME");
|
||||
|
||||
/**
|
||||
* Gets a unique string that identifies the {@link Session}
|
||||
*
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.FindByPrincipalNameSessionRepository;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
@@ -246,23 +247,28 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
|
||||
public class RedisOperationsSessionRepository implements FindByPrincipalNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
|
||||
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
|
||||
|
||||
/**
|
||||
* The prefix for each key in Redis used by Spring Session
|
||||
*/
|
||||
static final String SPRING_SESSION_KEY_PREFIX = "spring:session:";
|
||||
|
||||
/**
|
||||
* The prefix for each key that contains a mapping of the Principal name (i.e. username) to the session ids.
|
||||
*/
|
||||
static final String PRINCIPAL_NAME_PREFIX = SPRING_SESSION_KEY_PREFIX + "index:" + Session.PRINCIPAL_NAME_ATTRIBUTE_NAME + ":";
|
||||
|
||||
/**
|
||||
* The prefix for SessionCreated event channel. The suffix is the session id.
|
||||
*/
|
||||
private static final String SPRING_SESSION_CREATED_PREFIX = "spring:session:event:created:";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
|
||||
public void publishEvent(ApplicationEvent event) {
|
||||
}
|
||||
};
|
||||
private static final String SPRING_SESSION_CREATED_PREFIX = SPRING_SESSION_KEY_PREFIX + "event:created:";
|
||||
|
||||
/**
|
||||
* The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
|
||||
*/
|
||||
static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:";
|
||||
static final String BOUNDED_HASH_KEY_PREFIX = SPRING_SESSION_KEY_PREFIX + "sessions:";
|
||||
|
||||
/**
|
||||
* The key in the Hash representing {@link org.springframework.session.ExpiringSession#getCreationTime()}
|
||||
@@ -290,6 +296,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
|
||||
private final RedisSessionExpirationPolicy expirationPolicy;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
|
||||
public void publishEvent(ApplicationEvent event) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for {@link RedisSession#setMaxInactiveIntervalInSeconds(int)}.
|
||||
*/
|
||||
@@ -357,6 +368,24 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
return getSession(id, false);
|
||||
}
|
||||
|
||||
public Map<String,RedisSession> findByPrincipalName(String principalName) {
|
||||
String principalKey = getPrincipalKey(principalName);
|
||||
Set<Object> sessionIds = sessionRedisOperations.boundSetOps(principalKey).members();
|
||||
Map<String,RedisSession> sessions = new HashMap<String,RedisSession>(sessionIds.size());
|
||||
for(Object id : sessionIds) {
|
||||
RedisSession session = getSession((String) id);
|
||||
if(session != null) {
|
||||
session.setLastAccessedTime(session.originalLastAccessTime);
|
||||
sessions.put(session.getId(), session);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
private String getPrincipalKey(String principalName) {
|
||||
return PRINCIPAL_NAME_PREFIX + principalName;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id the session id
|
||||
@@ -376,7 +405,7 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
return null;
|
||||
}
|
||||
RedisSession result = new RedisSession(loaded);
|
||||
result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveIntervalInSeconds());
|
||||
result.originalLastAccessTime = loaded.getLastAccessedTime();
|
||||
result.setLastAccessedTime(System.currentTimeMillis());
|
||||
return result;
|
||||
}
|
||||
@@ -459,6 +488,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
|
||||
}
|
||||
|
||||
String principal = (String) session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
|
||||
if(principal != null) {
|
||||
sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
|
||||
}
|
||||
|
||||
if(isDeleted) {
|
||||
handleDeleted(sessionId, session);
|
||||
} else {
|
||||
@@ -640,9 +674,17 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
private void saveDelta() {
|
||||
String sessionId = getId();
|
||||
getSessionBoundHashOperations(sessionId).putAll(delta);
|
||||
String key = getSessionAttrNameKey(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
|
||||
if(delta.containsKey(key)) {
|
||||
Object principal = delta.get(key);
|
||||
String principalKey = getPrincipalKey((String) principal);
|
||||
sessionRedisOperations.boundSetOps(principalKey).add(sessionId);
|
||||
}
|
||||
|
||||
delta = new HashMap<String,Object>(delta.size());
|
||||
|
||||
expirationPolicy.onExpirationUpdated(originalLastAccessTime, this);
|
||||
Long originalExpiration = originalLastAccessTime == null ? null : originalLastAccessTime + TimeUnit.SECONDS.toMillis(getMaxInactiveIntervalInSeconds()) ;
|
||||
expirationPolicy.onExpirationUpdated(originalExpiration, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import static org.springframework.session.data.redis.RedisOperationsSessionRepos
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.getSessionAttrNameKey;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
@@ -308,6 +309,46 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
assertThat(redisRepository.getSession(expiredId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByPrincipalNameExpired() {
|
||||
String expiredId = "expired-id";
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(boundSetOperations.members()).thenReturn(Collections.<Object>singleton(expiredId));
|
||||
when(redisOperations.boundHashOps(getKey(expiredId))).thenReturn(boundHashOperations);
|
||||
Map map = map(
|
||||
MAX_INACTIVE_ATTR, 1,
|
||||
LAST_ACCESSED_ATTR, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
|
||||
when(boundHashOperations.entries()).thenReturn(map);
|
||||
|
||||
assertThat(redisRepository.findByPrincipalName("principal")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByPrincipalName() {
|
||||
long lastAccessed = System.currentTimeMillis() - 10;
|
||||
long createdTime = lastAccessed - 10;
|
||||
int maxInactive = 3600;
|
||||
String sessionId = "some-id";
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(boundSetOperations.members()).thenReturn(Collections.<Object>singleton(sessionId));
|
||||
when(redisOperations.boundHashOps(getKey(sessionId))).thenReturn(boundHashOperations);
|
||||
Map map = map(
|
||||
CREATION_TIME_ATTR, createdTime,
|
||||
MAX_INACTIVE_ATTR, maxInactive,
|
||||
LAST_ACCESSED_ATTR, lastAccessed);
|
||||
when(boundHashOperations.entries()).thenReturn(map);
|
||||
|
||||
Map<String, RedisSession> sessionIdToSessions = redisRepository.findByPrincipalName("principal");
|
||||
|
||||
assertThat(sessionIdToSessions).hasSize(1);
|
||||
RedisSession session = sessionIdToSessions.get(sessionId);
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getId()).isEqualTo(sessionId);
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(lastAccessed);
|
||||
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(maxInactive);
|
||||
assertThat(session.getCreationTime()).isEqualTo(createdTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupExpiredSessions() {
|
||||
String expiredId = "expired-id";
|
||||
|
||||
Reference in New Issue
Block a user