satoshi
satoshi

Reputation: 4103

Online users with Spring Security

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

Answers (2)

Erik
Erik

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

satoshi
satoshi

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

Related Questions