Saturday, November 13, 2010

JMX - Counting Users

Have you ever asked yourself how many users are using your favorite service at the moment? Watching the leaves falling down outside your window you ask yourself - am I alone? Is there anyone logged in except me? Hey! Stop watching the leaves, they will only make you more melancholic ;) - use the JMX :)

Read this article, and you'll discover the idea of controlling number of users logged in for web application based on Spring FrameworkSpring Security and Java Management Extensions (JMX).

First of all, we'll define the Managed Bean:
@ManagedResource(objectName = "Test:name=BigBrother")
public class BigBrother implements NotificationPublisherAware {

    private NotificationPublisher notificationPublisher;

    private final AtomicInteger userCounter = new AtomicInteger(0);

    @ManagedAttribute
    public int getUserCounter() {
        return userCounter.intValue();
    }

    @Override
    public void setNotificationPublisher(NotificationPublisher notificationPublisher) {
        this.notificationPublisher = notificationPublisher;
    }

    public void userLoggedIn(Principal principal) {
        userCounter.incrementAndGet();
        notificationPublisher.sendNotification(new Notification("User '" + principal.getName() + "' logged in.", this, 0));
    }

    public void userLoggedOut(Principal principal) {
        userCounter.decrementAndGet();
        notificationPublisher.sendNotification(new Notification("User '" + principal.getName() + "' logged out.", this, 0));
    }

}
Now we have to call the appropriate methods of this bean each time user will log in or log out.

First case is definitely the easiest one :) We may use the default behavior of Spring Security. Each time user is successfully authenticated, the Spring Security tries to publish an application event related to it. Happily for us when you use Security Namespace Configuration the default event publisher used (DefaultAuthenticationEventPublisher) sends the AuthenticationSuccessEvent. 
This type of events can be observed by the bean similar to:
public class LoginListener implements ApplicationListener<AuthenticationSuccessEvent> {

    @Autowired
    private BigBrother bigBrother;

    @Override
    public void onApplicationEvent(AuthenticationSuccessEvent event) {
        bigBrother.userLoggedIn(event.getAuthentication());
    }
}
All you have to do is register the above bean in Spring context :)

Now it's time to face the handling of user's log out. We could use nice feature of Spring Security LogoutFilter - each time user log out is handled by it, and the log out finishes successfully, the defined LogoutSuccessHandler is called. You may think - It can't be so easy - Exactly, my dear Watson, it can't be :)

You have to remember that users usually do what they want, and not what you expect them to do :) They don't have to log out from your application, then can just close the browser window, or shutdown the computer, or just leave the browser alone for so long that HTTP session will expire.

The expiration is the clue :) We can use HttpSessionBindingListener interface - if we add object implementing this interface to the HTTP session, it will be notified when it will be removed from session, which can be triggered when: a) session will be invalidated on user's log out (default behavior of  Spring Security configured using Security Namespace Configuration), b) session expires after some time of user inactivity, c) session attribute is removed on developer's demand

Let's start with the following XML configuration:
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       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-3.0.xsd
           http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
    ...
    <security:http auto-config='true'>
        ...
        <security:custom-filter ref="security.BigBrotherHelpingFilter" after="SECURITY_CONTEXT_FILTER" />
        ...                
    </security:http>
    ...                            
    <bean id="security.BigBrotherHelpingFilter" class="....BigBrotherHelpingFilter" />
</beans>
The above XML will add our own authentication processing filter into Spring Security filters chain, right after the SecurityContextPersistenceFilter (see: Adding in Your Own Filters). It will be something like this:
public class BigBrotherHelpingFilter extends GenericFilterBean {

    private String attributeName = "__BIG_BROTHER_HELPER";

    @Autowired
    private BigBrother bigBrother;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
                    ServletException {
        chain.doFilter(request, response);
        if (((HttpServletResponse) response).isCommitted()) {
            return;
        }
        HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
        if (null == httpSession) {
            return;
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (null != authentication) {
            httpSession.setAttribute(attributeName, new AuthenticationHolder(authentication, bigBrother));
        } else {
            httpSession.setAttribute(attributeName, null);
        }
    }
    ...
}
And the last part of code for this post:
public class AuthenticationHolder implements HttpSessionBindingListener {

    private final Authentication authentication;

    private final BigBrother bigBrother;

    public AuthenticationHolder(Authentication authentication, BigBrother bigBrother) {
        this.authentication = authentication;
        this.bigBrother = bigBrother;
    }

    public Authentication getAuthentication() {
        return authentication;
    }

    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        // Ignore ...
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        bigBrother.userLoggedOut(authentication);
    }

}

Let's test the whole setup using VisualVM - when you run it, select your servlet container, and then available managed beans, under Test/BigBrother you should see the bean used by us:


Let's subscribe to the notifications in Notifications tab, and try to log in to our web application, you should see new notification:


When you select the Operations tab, and run getUserCounter operation you'll receive:

Now let's try to log out, then check the notifications:


and the user counter:
You can observe similar effect to log out when you leave your browser untouched until your HTTP session will expire (by default 30 minutes). 

1 comment:

  1. Your post is very helpful, except when I implement it, I get the following error:

    Jun 23, 2014 1:56:33 PM org.apache.catalina.session.StandardSession setAttribute
    SEVERE: Session binding event listener threw exception
    java.lang.NullPointerException
    at com.periscope.java.counter.AuthenticationHolder.valueUnbound(AuthenticationHolder.java:31)
    at org.apache.catalina.session.StandardSession.setAttribute(StandardSession.java:1507)
    at org.apache.catalina.session.StandardSession.setAttribute(StandardSession.java:1441)

    Do you know why I'm getting this error? I followed your tutorial word for word.

    ReplyDelete