Reputation: 4103
I'm using Spring Security and I would like to know which users are currently online. I first tried the approach using SessionRegistryImpl
and <session-management session-authentication-strategy-ref="..." ... />
, but I guess this List is stored in memory and I would like to avoid it (it's going to be a huge website and a lot of users will be online at the same time, the List can become huge). Please correct me if I'm wrong.
The second approach I tried is using a listener and the HttpSessionListener
interface and a custom AuthenticationManager
and storing the "is online flag" in the database. Basically, the flag is set to true in the authenticate(...)
method of my authentication manager and set to false in the sessionDestroyed(...)
method of my session listener.
web.xml:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Test</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/security.xml</param-value>
</context-param>
<!-- Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>my.package.SessionListener</listener-class>
</listener>
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>1</session-timeout>
</session-config>
</web-app>
Spring Security configuration:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<beans:bean id="authenticationManager" class="my.package.security.AuthenticationManager" />
<http disable-url-rewriting="true" authentication-manager-ref="authenticationManager">
<!--<intercept-url pattern="/" access="ROLE_ANONYMOUS" />-->
<intercept-url pattern="/login*" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/favicon.ico" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/*" access="ROLE_USER" />
<form-login login-processing-url="/authorize" login-page="/login" authentication-failure-url="/login-failed" />
<logout logout-url="/logout" logout-success-url="/login" />
<remember-me data-source-ref="dataSource" />
</http>
</beans:beans>
my.package.SessionListener:
public class SessionListener implements HttpSessionListener
{
public void sessionCreated(HttpSessionEvent httpSessionEvent)
{
}
public void sessionDestroyed(HttpSessionEvent httpSessionEvent)
{
UserJpaDao userDao = WebApplicationContextUtils.getWebApplicationContext(httpSessionEvent.getSession().getServletContext()).getBean(UserJpaDao.class);
Authentication a = SecurityContextHolder.getContext().getAuthentication();
if(a != null)
{
User loggedInUser = userDao.findByAlias(a.getName());
if(loggedInUser != null)
{
loggedInUser.setOnline(false);
userDao.save(loggedInUser);
}
}
}
}
my.package.security.AuthenticationManager:
public class AuthenticationManager implements org.springframework.security.authentication.AuthenticationManager
{
@Autowired
UserJpaDao userDao;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
User loggedInUser = null;
Collection<? extends GrantedAuthority> grantedAuthorities = null;
...
loggedInUser = userDao.findByAlias(authentication.getName());
if(loggedInUser != null)
{
// Verify password etc.
loggedInUser.setOnline(true);
userDao.save(loggedInUser);
}
else
{
loggedInUser = null;
throw new BadCredentialsException("Unknown username");
}
return new UsernamePasswordAuthenticationToken(loggedInUser, authentication.getCredentials(), grantedAuthorities);
}
}
sessionCreated
and sessionDestroyed
are fired correctly, but SecurityContextHolder.getContext().getAuthentication();
is always null.
Update: almost everything is working perfectly. The only problem is that when the session expires due to timeout, SecurityContextHolder.getContext().getAuthentication()
returns null in the sessionDestroyed(...)
method. It works perfectly when the logout is manually triggered.
Can someone help me? Any hint is greatly appreciated.
Thank you
Upvotes: 3
Views: 7683
Reputation: 1017
You can't use the SecurityContextHolder
to get the Principal in your SessionListener
since that is only valid in the context of a request.
All the info you need is in the Session itself
Example:
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
SecurityContext context = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");
Authentication authentication = context.getAuthentication();
Object principal = authentication.getPrincipal();
// Your code goes here
}
Upvotes: 5
Reputation: 4103
I decided to undertake the session registry method (only because I was not able to make the other method work). Here is my code (important parts).
web.xml:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Test</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/security.xml
</param-value>
</context-param>
...
<!-- Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>1</session-timeout>
</session-config>
</web-app>
security.xml:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<beans:bean id="authenticationManager" class="my.package.security.AuthenticationManager" />
<beans:bean id="userDetailsDao" class="my.package.dao.UserDetailsDao" />
<http disable-url-rewriting="true" authentication-manager-ref="authenticationManager">
<intercept-url pattern="/login*" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/favicon.ico" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/*" access="ROLE_USER" />
<form-login login-processing-url="/authorize" login-page="/login" authentication-failure-url="/login-failed" />
<logout logout-url="/logout" logout-success-url="/login" />
<remember-me data-source-ref="dataSource" user-service-ref="userDetailsDao" />
<session-management session-authentication-strategy-ref="sas" invalid-session-url="/invalid-session" />
</http>
<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:property name="maximumSessions" value="1" />
</beans:bean>
</beans:beans>
my.package.security.AuthenticationManager:
public class AuthenticationManager implements org.springframework.security.authentication.AuthenticationManager
{
@Autowired
UserJpaDao userDao;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
UserDetails userDetails = null;
if(authentication.getPrincipal() == null || authentication.getCredentials() == null)
{
throw new BadCredentialsException("Invalid username/password");
}
User loggedInUser = userDao.findByAlias(authentication.getName());
if(loggedInUser != null)
{
// TODO: check credentials
userDetails = new UserDetails(loggedInUser);
}
else
{
loggedInUser = null;
throw new BadCredentialsException("Unknown username");
}
return new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
}
}
my.package.dao.UserDetailsDao (this is needed only for the remember-me functionality):
public class UserDetailsDao implements UserDetailsService
{
@Autowired
UserJpaDao userDao;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User user = userDao.findByAlias(username);
if(user != null)
{
return new UserDetails(user);
}
throw new UsernameNotFoundException("The specified user cannot be found");
}
}
my.package.UserDetails:
public class UserDetails implements org.springframework.security.core.userdetails.UserDetails
{
private String alias;
private String encryptedPassword;
public UserDetails(User user)
{
this.alias = user.getAlias();
this.encryptedPassword = user.getEncryptedPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return authorities;
}
@Override
public String getPassword()
{
return this.encryptedPassword;
}
@Override
public String getUsername()
{
return this.alias;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
}
sessionRegistry.getAllPrincipals()
will return a List<Object>
"castable" to List<UserDetails>
.
My code to get a list of online users, where the objects of type class User
are persisted in the database through JPA:
List<User> onlineUsers = userDao.findByListOfUserDetails((List<UserDetails>)(List<?>)sessionRegistry.getAllPrincipals());
Note: sessionRegistry
is an Autowired implementation of the class SessionRegistryImpl
.
Note: for the remember-me functionality, I'm using the persistent token approach. A persistent_logins
is needed in the database (see 10.3 Persistent Token Approach).
Hope this may be useful to someone else.
Upvotes: 1