Here's how to support log-in and log-out even when the view has expired
stephen Jan 2, 2009 11:17 PMHere's how to support log-in and log-out even when the view has expired
Put mildly, I think it is very annoying to click on a log-out
and be answered
with an error message that tells me that I have to log-in first.
Trying to log-in and being shown the log-in page again because the previous view
state has already expired is an equally bad user experience.
I have been struggling with these issues and finally found solutions.
Here they are. They might be helpful for somebody else or there might be a
better/easier way to achieve the same.
Note that there is a facelets parameter, that should automatically provide
the desired behaviour, but on first attempt it didn't work for me and the bug
mentioned near the end of this thread sounds daunting:
Nabble: PARAM_BUILD_BEFORE_RESTORE
Log-out with expired view
This is easy to handle using Seam:
Just make the log-out link a plain (GET) link (instead of a command link):
<h:outputLink value="#{request.contextPath}/log-out" rendered="#{identity.loggedIn}">
Log-Out
</h:outputLink>
and handle the actual log-out in a page action (pages.xml):
<page view-id="/log-out.xhtml">
<rewrite pattern="/log-out"/>
<action execute="#{authenticator.logout}"/>
</page>
where authenticator.logout is simply
public String logout() {
Session.instance().invalidate();
return null;
}
Instead of using an outputLink with a page action it would probably
work just as well to use <s:link action="#{authenticator.logout}"...
Log-in with expired view
This one is a little tougher: You can't use a GET request, because naturally
passwords should never be part of the URL. However a JSF postback always
requires the view state to be restored.
So I fell back to writing the log-in form in plain html. You can actually use
JSF components for input fields and labels, but use a plain html form tag
instead of h:form, so that the action can be customized and the POST request
handled outside
of JSF.
This is in /log-in.xhtml:
<form method="post" action="/acme/do-log-in" >
<h:inputText id="nameInput"/>
<h:inputText id="passwordInput"/>
<h:inputHidden id="cid" value="#{conversation.id}"/>
<h:commandButton id="loginButton" value="Login"/>
</form>Labels, messages, and layout excluded for brevity.
Be sure to pass the conversation id in a hidden field (the name of that parameter is configured in components.xml: <core:manager ... conversation-id-parameter="cid"/>)
To handle the POST request my first attempt was a custom servlet (integrated
in Seam by configuring the ContextFilter), but that did not work because the
events and redirects that occur after a login attempt require an active
FacesContext.
But fortunately a Seam page action can handle POST requests, too:
<page view-id="/do-log-in.xhtml" action="#{authenticator.doLogIn}">
<rewrite pattern="/do-log-in"/>
<navigation from-action="#{authenticator.doLogIn}">
<rule if="#{not identity.loggedIn}">
<redirect view-id="/log-in.xhtml"/>
</rule>
<rule if="#{identity.loggedIn and empty redirect.viewId}">
<!-- Either the user hit the log-in page directly, or the session has expired while the log-in page sat idle -->
<redirect view-id="/contact-data.xhtml"/>
</rule>
</navigation>
</page>
and the Authenticator.doLogIn() just grabs the request values, validates them
(we are bypassing JSF, so no help from JSF validators), stuffs them into the
credentials and calls identity.login():
@In private Credentials credentials;
@In private FacesMessages facesMessages;
@In private Identity identity;
public void doLogIn() {
boolean valid = true;
String userName = FacesUtil.getRequestParameter("nameInput");
if (Stringz.isEmpty(userName)) {
valid = false;
facesMessages.addToControl("nameInput", StatusMessage.Severity.ERROR, "Please enter a value");
}
String password = FacesUtil.getRequestParameter("passwordInput");
if (Stringz.isEmpty(password)) {
valid = false;
facesMessages.addToControl("passwordInput", StatusMessage.Severity.ERROR, "Please enter a value");
}
if (!valid) {
return;
}
credentials.setUsername(userName);
credentials.setPassword(password);
String result = identity.login();
}
When testing the validation, you'll see that the faces messages are not added to
the controls, but will end up as global messages. That is a known bug in Seam:
JBSEAM-1855
Tada!
I just don't really want to think about the fact that these are problems that
never would have happened if I used plain JSP pages. JSF comes at a cost.
(Then again it was not so much harder than getting all the formatting instructions for this forum message right.)