20

I'm using JSF 1.2 with Richfaces and Facelets.

I have an application with many session-scoped beans and some application beans.

The user logs in with, let's say, Firefox. A session is created with ID="A"; Then he opens Chrome and logs in again with the same credentials. A session is created with ID="B".

When the session "B" is created, I want to be able to destroy session "A". How to do that?

Also. when the user in Firefox does anything, I want to be able to display a popup or some kind of notification saying "You have been logged out because you have logged in from somewhere else".

I have a sessionListener who keeps track of the sessions created and destroyed. The thing is, I could save the HTTPSession object in a application-scoped bean and destroy it when I detect that the user has logged in twice. But something tells me that is just wrong and won't work.

Does JSF keep track of the sessions somewhere on the server side? How to access them by identifier? If not, how to kick out the first log in of an user when he logs in twice?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
pakore
  • 11,395
  • 12
  • 43
  • 62

3 Answers3

20

The DB-independent approach would be to let the User have a static Map<User, HttpSession> variable and implement HttpSessionBindingListener (and Object#equals() and Object#hashCode()). This way your webapp will still function after an unforeseen crash which may cause that the DB values don't get updated (you can of course create a ServletContextListener which resets the DB on webapp startup, but that's only more and more work).

Here's how the User should look like:

public class User implements HttpSessionBindingListener {

    // All logins.
    private static Map<User, HttpSession> logins = new ConcurrentHashMap<>();

    // Normal properties.
    private Long id;
    private String username;
    // Etc.. Of course with public getters+setters.

    @Override
    public boolean equals(Object other) {
        return (other instanceof User) && (id != null) ? id.equals(((User) other).id) : (other == this);
    }

    @Override
    public int hashCode() {
        return (id != null) ? (this.getClass().hashCode() + id.hashCode()) : super.hashCode();
    }

    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        HttpSession session = logins.remove(this);
        if (session != null) {
            session.invalidate();
        }
        logins.put(this, event.getSession());
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        logins.remove(this);
    }

}

When you login the User as follows:

User user = userDAO.find(username, password);
if (user != null) {
    sessionMap.put("user", user);
} else {
    // Show error.
}

then it will invoke the valueBound() which will remove any previously logged in user from the logins map and invalidate the session.

When you logout the User as follows:

sessionMap.remove("user");

or when the session is timed out, then the valueUnbound() will be invoked which removes the user from the logins map.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Thanks for the answer. I guess that "sessionMap.put("user", user);" should be "sessionMap.put(username, user);". Otherwise if a different user with different credentials logs in we would kick the first user out. – pakore Mar 04 '10 at 11:06
  • That's not normal. You don't want to have different logged in users during one client session. Also don't confuse the session map with the application map. – BalusC Mar 04 '10 at 11:24
  • Ok I understand now that sessionMap is ExternalContext.sessionMap .It works :). – pakore Mar 04 '10 at 14:41
  • When `valueUnbound()` is called, dont you want to `invalidate` the session as well? – Thang Pham Sep 07 '10 at 16:12
  • @Harry: No. Even more, it's in fact the invalidation itself which caused the method to be called :) – BalusC Sep 07 '10 at 16:16
  • @balusC: What does `getExternalContext().getSessionMap()` return? Does it return `Map logins` inside `User`? – Thang Pham Sep 10 '10 at 15:23
  • No, a `Map` with session scoped attributes. It's basically a back-mapping of `HttpSession#setAttribute()/#getAttribute()` as known from the "raw" Servlet API. If a session attribute implements `HttpSessionBindingListener`, then the appropriate `valueBound()/valueUnbound()` methods will be called during insertion/removal of the session scope. This is just part of the "raw" Servlet API, not JSF. JSF runs on top of Servlet API. – BalusC Sep 10 '10 at 15:29
  • @BalusC: Don't we need to protect the HashMap against concurrent modifications? – AndrewBourgeois May 29 '12 at 13:26
  • @AndrewBourgeois: that would only form a problem when the same user logs in twice at *exactly the same moment*, which is very unlikely. – BalusC May 29 '12 at 13:29
  • @BalusC: I never trust my users not to try and do weird things. ;) – AndrewBourgeois May 29 '12 at 13:39
  • @BalusC thanks explanation. still its doesnot understand/work if we follow above steps as its, because its very high level expanation... It could like : when the User Object is added to the session via session.setAttribute() the valueBound() method is invoked for that specific object. And the valueUnbound() is invoked every time a User Object is removed from the session either via session.removeAttribute() or session.invalidate() Ex should be: if (user != null) { session.setAttribute(userName, user); } else { // Show error. } please correct me I'm wrong. – Ramnath Nov 26 '20 at 04:02
4
  1. create an integer field in the databse userLoggedInCount
  2. On each login increment that flag and store the result in the session.
  3. On each request check the value in the database and the one in the session, and if the one in the session is less than the one in the DB, invalidate() the session and decrement the value in the database
  4. whenever a session is destroyed decrement the value as well.
Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • instead of DB i can use an application scoped class I guess. The thing is how to access it from a phase listener for example? Good solution though. – pakore Mar 03 '10 at 15:35
  • well, you can obtain the ExternalContext via the FacesContext – Bozho Mar 03 '10 at 16:09
  • 1
    This algorithm might not be sufficient. The theory is perfect, but in the practice, it won't work if the browser saves the session in a cookie and restores it back from it. Take a look to Step 2. If, as I said, the browse restores the session automatically, it won't pass through any LoginHandler or LoginMethod or any controllable place to perform the increment of that flag. How to act in this case? – ElPiter Dec 29 '12 at 14:05
1

I like the answer from BalusC with a HttpSessionBindingListener.

But in Enterprise JavaBeansTM Specification, Version 2.0 there is written:

An enterprise Bean must not use read/write static fields. Using read-only static fields is allowed. Therefore, it is recommended that all static fields in the enterprise bean class be declared as final

So isnt't it better to make an ApplicationScoped Bean which store the table application wide without using static fields???

It tried it out and it seems to work...

Here is my example:

@Named
@ApplicationScoped
public class UserSessionStorage implements java.io.Serializable,HttpSessionBindingListener {

@Inject
UserManagement userManagement;

private static final long serialVersionUID = 1L;

/**
 * Application wide storage of the logins
 */
private final Map<User, List<HttpSession>> logins = new HashMap<User, List<HttpSession>>();

@Override
public void valueBound(final HttpSessionBindingEvent event) {
    System.out.println("valueBound");

    /**
     * Get current user from userManagement...
     */
    User currentUser = userManagement.getCurrentUser();

    List<HttpSession> sessions = logins.get(currentUser);
    if (sessions != null) {
        for (HttpSession httpSession : sessions) {
            httpSession.setAttribute("invalid", "viewExpired");
        }
    } else {
        sessions = new ArrayList<HttpSession>();
    }
    HttpSession currentSession = event.getSession();
    sessions.add(currentSession);
    logins.put(currentUser, sessions);
}

@Override
public void valueUnbound(final HttpSessionBindingEvent event) {
    System.out.println("valueUnbound");

    User currentUser = userManagement.getCurrentUser();

    List<HttpSession> sessions = logins.get(currentUser);
    if (sessions != null) {
        sessions.remove(event.getSession());
    } else {
        sessions = new ArrayList<HttpSession>();
    }
    logins.put(currentUser, sessions);
}

}

-> Sorry for my änglish...

Gatschet
  • 1,337
  • 24
  • 38
  • 1
    A `HttpSessionBindingListener` is not an EJB. Only those classes with a `javax.ejb.*` main annotation such as `@Stateless` are EJBs. – BalusC Jun 17 '13 at 11:05