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.)