Controlling access to web resources with a a login process is a common use-case. Implementing this using an interceptor in Struts 2 is very straight forward.
The parts of the solution:
In Struts 2 interceptors are called one after the other in a configured order for each invocation of an action class. The interceptors have an opportunity to interact with the input from your user, and to interact with the action after it has completed it's processing. They can also intercept the process between the running of your action and the hand off to the result, but I we will not need that for our purposes here.
The main method in an interceptor is the intercept() method. This method is passed an ActionInvocation object, which contains the action, and the context for the action, such as the HttpRequest, HttpSession, etc. The intercept method returns a String that maps to a "result" in our struts.xml file.
The intercept() method for our interceptor uses the HttpSession to determine if the user's "user" object is stored in the session. If the user object is in the HttpSession, then we consider the user to be logged in, and continue the chain of the interceptor stack by calling invocation.invoke ().
If the user's "user" object isn't in the HttpSession we check to see if the user is attempting to log in, and process the login. If they're username and password validate the interceptor sends them the to the "login-success" result, otherwise we send them the "login" result.
public String intercept (ActionInvocation invocation) throws Exception {
// Get the action context from the invocation so we can access the
// HttpServletRequest and HttpSession objects.
final ActionContext context = invocation.getInvocationContext ();
HttpServletRequest request = (HttpServletRequest) context.get(HTTP_REQUEST);
HttpSession session = request.getSession (true);
// Is there a "user" object stored in the user's HttpSession?
Object user = session.getAttribute (USER_HANDLE);
if (user == null) {
// The user has not logged in yet.
// Is the user attempting to log in right now?
String loginAttempt = request.getParameter (LOGIN_ATTEMPT);
if (! StringUtils.isBlank (loginAttempt) ) { // The user is attempting to log in.
// Process the user's login attempt.
if (processLoginAttempt (request, session) ) {
// The login succeeded send them the login-success page.
return "login-success";
} else {
// The login failed. Set an error if we can on the action.
Object action = invocation.getAction ();
if (action instanceof ValidationAware) {
((ValidationAware) action).addActionError ("Username or password incorrect.");
}
}
}
// Either the login attempt failed or the user hasn't tried to login yet,
// and we need to send the login form.
return "login";
} else {
return invocation.invoke ();
}
}
The actual validation of the username and password are left to the processLoginAttempt() method. The processLoginAttempt() method is responsible for using whatever means necessary to validate the username and password and saving the user's user object in the session. This is done this way, because there might be many ways to do the validation, and you might collect additional information on the user to store in their sesion, such as roles, or other credentials. Breaking this out allows you to override this method in your own interceptor and process this however you'd like.
The default implementation uses a SecurityManager to lookup the user's information.
/**
* Attempt to process the user's login attempt delegating the work to the
* SecurityManager.
*/
public boolean processLoginAttempt (HttpServletRequest request, HttpSession session) {
// Get the username and password submitted by the user from the HttpRequest.
String username = request.getParameter (USERNAME);
String password = request.getParameter (PASSWORD);
// Use the security manager to validate the user's username and password.
Object user = securityManager.login (username, password);
if (user != null) {
// The user has successfully logged in. Store their user object in
// their HttpSession. Then return true.
session.setAttribute (USER_HANDLE, user);
return true;
} else {
// The user did not successfully log in. Return false.
return false;
}
}
In our struts.xml configuration file we need to declare the LoginInterceptor, and add it to an interceptor class so it will be used.
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
...
<interceptor name="login" class="loginInterceptor" />
...
</interceptors>
</package>
</struts>
Notice that the fully qualified classname is not declared for the interceptor. We configure that in our Spring config, which allows us to wire our SecurityManager into it.
We need to add the LoginInterceptor to an interceptor stack so it will be used when an action is invoked.
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
...
<interceptor-stack name="defaultLoginStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="login" /> <!-- Our LoginInterceptor -->
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
...
</package>
</struts>
We need to add a global "login" result so that it is available no matter what protected action a user might invoke. This allows our interceptor to return "login" and have it properly mapped to our login page.
<struts>
<package name="my-default" extends="struts-default">
...
<global-results>
<result name="login">/WEB-INF/pages/Login.jsp</result>
<result name="login-success">/WEB-INF/pages/Index.jsp</result>
</global-results>
...
</package>
</struts>
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
<interceptor name="login" class="loginInterceptor" />
<interceptor-stack name="defaultLoginStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="login" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
<interceptor-stack name="defaultInsecureStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
</interceptors>
<!-- Make the defaultLoginStack the default one used
for all actions unless otherwise configured. -->
<default-interceptor-ref name="defaultLoginStack" />
<default-action-ref name="index" />
<global-results>
<result name="login">/WEB-INF/pages/Login.jsp</result>
<result name="login-success">/WEB-INF/pages/Index.jsp</result>
</global-results>
<action name="index">
<result name="input">/WEB-INF/pages/Index.jsp</result>
<result name="success">/WEB-INF/pages/Index.jsp</result>
</action>
</package>
</struts>
We need a JSP page to allow the user to enter their username and password.
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Login</title>
</head>
<body>
<s:actionerror />
<s:form namespace="/" action="index.action" method="post">
<s:hidden name="QUADRAN_LOGIN_ATTEMPT" value="%{'1'}" />
<s:textfield name="QUADRAN_USERNAME" label="Username" />
<s:password name="QUADRAN_PASSWORD" label="Password" />
<s:submit value="Login" align="center">
<s:param name="colspan" value="%{2}" />
<s:param name="align" value="%{'center'}" />
</s:submit>
</s:form>
</body>
</html>
Notice the hidden QUADRAN_LOGIN_ATTEMP property in the form. That is what triggers the processing of the login by the LoginInterceptor.
I use plain POJO service beans and wire them using Spring. Here's a simple SecurityManager interface. Just enough to do the login. I leave the rest as a practice to the user.
package net.vitarara.quadran.core.business.api.security;
public interface SecurityManager {
public Object login (String username, String password);
}
And the implementation in Groovy:
package net.vitarara.quadran.core.business.impl.security;
import net.vitarara.quadran.core.business.api.security.SecurityManager;
class SecurityManagerImpl implements SecurityManager {
def partyDao;
Object login (String username, String password) {
return partyDao.findByUsernameAndPassword (username, password);
}
}
I delegate the lookup to my PartyDao which is a class for looking up all types of parties, users, customers, vendors, etc. You will need to figure out how you want to look users up in your system.
In my Spring applciationContext.xml file I have the following entry for my interceptor. This entry wires my SecurityManager into the interceptor allowing it to delegate the actual lookup to my business service beans.
<bean id="loginInterceptor" scope="singleton" class="net.vitarara.quadran.core.web.util.LoginInterceptor">
<property name="securityManager"><ref local="securityManager" /></property>
</bean>
If you are using the Struts 2 Spring plugin whe Struts attempts to load the interceptor at startup it will use Spring to instantiate it.
It's a few steps, but the basics are fairly straight forward.
Using this same pattern you can address many cross cutting issues in Struts 2. Such as:
The interceptor pattern is a very powerful one, well worth looking at. Struts 2 uses it to implement a great deal of its functionality.
| Attachment | Size |
|---|---|
| LoginInterceptor.zip | 3.31 KB |
Recent comments
1 week 6 days ago
2 weeks 4 days ago
3 weeks 12 hours ago
3 weeks 6 days ago
4 weeks 2 days ago
4 weeks 2 days ago
5 weeks 3 hours ago
8 weeks 1 day ago
8 weeks 6 days ago
8 weeks 6 days ago