Making Acegi work with ZK

From Documentation
DocumentationSmall Talks2007JanuaryMaking Acegi work with ZK
Making Acegi work with ZK

Author
Hari Gangadharan, software architect, Charles Schwab and Co. (San Francisco Bay Area, USA)
Date
January 08, 2007
Version


Introduction

In the last few weeks I have been trying to make Acegi work with ZK. Fairly quickly, I could make Acegi protect the ZK zul pages. When you are dealing with an AJAX toolkit just protecting pages does not mean anything. Actions are also important. You would like to protect many UI events you have. For example a voting button in your page is shown to all users. However you may want to direct the unauthenticated user to login page if that user clicks on the voting button.

Since ZK listeners give access to the components and events, a direct simple way to secure the actions was not available. From a security listener point of view, components and events are not that meaningful; the security listener is more interested in knowing which bean methods are executed by the UI events. When I looked into this, I felt we may be able to solve this by adding AOP based method security. But Acegi Method interceptors throw an AccessDeniedException to the UI when an unauthenticated user tries to access this action. To catch this exception and then redirect the user to login screen it became necessary to add a Front Controller.

Now let me walk you through setting up Acegi and ZK. Please complete the ZK quick start and make sure that ZK works in your container. I have used Hibernate, Spring and ZK in Tomcat. My Development environment was eclipse. If your environment is different, you may have to make adjustments accordingly.


Prerequisites

To start, follow the ZK Quickstart tutorial to complete the ZK setup. Make sure that ZK and Spring is working on your environment before you start following this document.


Add Acegi Security Filter to web.xml

Update the web.xml to add the Acegi security filter. If your application is using Spring, you should have the Spring context loader listener add to this already. See that I have added *.zul, /zkau/* and /j_security_check to the filter mapping. I have also added /WEB-INF/security.xml to the contextConfigLocations so that Spring will load the Acegi Security beans I have defined in security.xml.

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
  xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <display-name>wisdom-web</display-name>
  <!-- Context Configuration locations for Spring XML files -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/classes/applicationContext*.xml,
      /WEB-INF/security.xml
    </param-value>
  </context-param>

  <filter>
    <filter-name>securityFilter</filter-name>
    <filter-class>
      org.acegisecurity.util.FilterToBeanProxy
    </filter-class>
    <init-param>
      <param-name>targetClass</param-name>
      <param-value>
        org.acegisecurity.util.FilterChainProxy
      </param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>securityFilter</filter-name>
    <url-pattern>*.zul</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>securityFilter</filter-name>
    <url-pattern>/zkau/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>securityFilter</filter-name>
    <url-pattern>/j_security_check</url-pattern>
  </filter-mapping>

  <listener>
    <description>
      Used to cleanup when a session is destroyed
    </description>
    <display-name>ZK Session Cleaner</display-name>
    <listener-class>
      org.zkoss.zk.ui.http.HttpSessionListener
    </listener-class>
  </listener>

  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

  <servlet>
    <description>ZK loader for ZUML pages</description>
    <servlet-name>zkLoader</servlet-name>
    <servlet-class>
      org.zkoss.zk.ui.http.DHtmlLayoutServlet
    </servlet-class>
    <!-- Must. Specifies URI of the update engine (DHtmlUpdateServlet).
      It must be the same as <url-pattern> for the update engine.
    -->
    <init-param>
      <param-name>update-uri</param-name>
      <param-value>/zkau</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet>
    <description>The asynchronous update engine for ZK</description>
    <servlet-name>auEngine</servlet-name>
    <servlet-class>
      org.zkoss.zk.au.http.DHtmlUpdateServlet
    </servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>zkLoader</servlet-name>
    <url-pattern>*.zul</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>zkLoader</servlet-name>
    <url-pattern>*.zhtml</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>auEngine</servlet-name>
    <url-pattern>/zkau/*</url-pattern>
  </servlet-mapping>

  <!-- //// -->
  <!-- MIME mapping -->
  <mime-mapping>
    <extension>gif</extension>
    <mime-type>image/gif</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>html</extension>
    <mime-type>text/html</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>htm</extension>
    <mime-type>text/html</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>jpeg</extension>
    <mime-type>image/jpeg</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>jpg</extension>
    <mime-type>image/jpeg</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>js</extension>
    <mime-type>application/x-javascript</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>png</extension>
    <mime-type>image/png</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>txt</extension>
    <mime-type>text/plain</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>xml</extension>
    <mime-type>text/xml</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>zhtml</extension>
    <mime-type>text/html</mime-type>
  </mime-mapping>
  <mime-mapping>
    <extension>zul</extension>
    <mime-type>text/html</mime-type>
  </mime-mapping>
  <welcome-file-list>
    <welcome-file>index.zul</welcome-file>
    <welcome-file>index.zhtml</welcome-file>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
  </welcome-file-list>
</web-app>


Add security.xml

Now we need to add the security.xml. I use the following security.xml which is created from numerous examples I have seen in web.

It is interesting to see the definition of the filterInvocationInterceptor bean definition. You will see that I have put /create*, /delete* and /edit* as protected URL patterns. These patterns require user or admin role.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <!-- ======================== FILTER CHAIN ======================= -->
  <bean id="filterChainProxy"
    class="org.acegisecurity.util.FilterChainProxy">
    <property name="filterInvocationDefinitionSource">
      <value>
        CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
        PATTERN_TYPE_APACHE_ANT
        /**=httpSessionContextIntegrationFilter,logoutFilter,
        authenticationProcessingFilter,securityContextHolderAwareRequestFilter,
        rememberMeProcessingFilter,anonymousProcessingFilter,
        exceptionTranslationFilter,filterInvocationInterceptor
      </value>
    </property>
  </bean>

  <bean id="httpSessionContextIntegrationFilter"
    class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" />

  <bean id="logoutFilter"
    class="org.acegisecurity.ui.logout.LogoutFilter">
    <constructor-arg value="/index.zul" />
    <!-- URL redirected to after logout -->
    <constructor-arg>
      <list>
        <ref bean="rememberMeServices" />
        <bean
          class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
      </list>
    </constructor-arg>
    <property name="filterProcessesUrl" value="/logout.zul" />
  </bean>

  <bean id="authenticationProcessingFilter"
    class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
    <property name="authenticationManager"
      ref="authenticationManager" />
    <property name="authenticationFailureUrl"
      value="/login.zul?error=1" />
    <property name="defaultTargetUrl" value="/" />
    <property name="filterProcessesUrl" value="/j_security_check" />
    <property name="rememberMeServices" ref="rememberMeServices" />
  </bean>

  <bean id="securityContextHolderAwareRequestFilter"
    class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter" />

  <bean id="rememberMeProcessingFilter"
    class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
    <property name="authenticationManager"
      ref="authenticationManager" />
    <property name="rememberMeServices" ref="rememberMeServices" />
  </bean>

  <bean id="anonymousProcessingFilter"
    class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
    <property name="key" value="anonymous" />
    <property name="userAttribute" value="anonymous,ROLE_ANONYMOUS" />
  </bean>

  <bean id="exceptionTranslationFilter"
    class="org.acegisecurity.ui.ExceptionTranslationFilter">
    <property name="authenticationEntryPoint">
      <bean
        class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
        <property name="loginFormUrl" value="/login.zul?error=2" />
        <property name="forceHttps" value="false" />
      </bean>
    </property>
  </bean>

  <bean id="filterInvocationInterceptor"
    class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
    <property name="authenticationManager"
      ref="authenticationManager" />
    <property name="accessDecisionManager"
      ref="accessDecisionManager" />
    <property name="objectDefinitionSource">
      <value>
        PATTERN_TYPE_APACHE_ANT
        /welcome.zul=ROLE_ANONYMOUS,admin,user
        /signup.action=ROLE_ANONYMOUS,admin,user
        /login.zul=ROLE_ANONYMOUS,admin,user
        /show*=ROLE_ANONYMOUS,admin,user
        /create*=admin,user
        /edit*=admin,user
        /delete*=admin,user
        /zkau/*=ROLE_ANONYMOUS,admin,user
      </value>
    </property>
  </bean>

  <bean id="accessDecisionManager"
    class="org.acegisecurity.vote.AffirmativeBased">
    <property name="allowIfAllAbstainDecisions" value="false" />
    <property name="decisionVoters">
      <list>
        <bean class="org.acegisecurity.vote.RoleVoter">
          <property name="rolePrefix" value="" />
        </bean>
      </list>
    </property>
  </bean>

  <bean id="rememberMeServices"
    class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
    <property name="userDetailsService" ref="userManager" />
    <property name="key" value="wisdom-key" />
    <property name="parameter" value="rememberMe" />
  </bean>

  <bean id="authenticationManager"
    class="org.acegisecurity.providers.ProviderManager">
    <property name="providers">
      <list>
        <ref local="daoAuthenticationProvider" />
        <ref local="anonymousAuthenticationProvider" />
        <ref local="rememberMeAuthenticationProvider" />
      </list>
    </property>
  </bean>

  <bean id="daoAuthenticationProvider"
    class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
    <property name="userDetailsService" ref="userManager" />
  </bean>

  <bean id="anonymousAuthenticationProvider"
    class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
    <property name="key" value="anonymous" />
  </bean>

  <bean id="rememberMeAuthenticationProvider"
    class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
    <property name="key" value="wisdom-key" />
  </bean>


  <!-- This bean is optional; it isn't used by any other bean as it only listens and logs -->
  <bean id="loggerListener"
    class="org.acegisecurity.event.authentication.LoggerListener" />


  <!-- SSL Switching: to use this, configure it in the filterChainProxy bean -->
  <bean id="channelProcessingFilter"
    class="org.acegisecurity.securechannel.ChannelProcessingFilter">
    <property name="channelDecisionManager"
      ref="channelDecisionManager" />
    <property name="filterInvocationDefinitionSource">
      <value>
        PATTERN_TYPE_APACHE_ANT
        /admin/**=REQUIRES_SECURE_CHANNEL
        /login*=REQUIRES_SECURE_CHANNEL
        /j_security_check*=REQUIRES_SECURE_CHANNEL
        /**=REQUIRES_INSECURE_CHANNEL
      </value>
    </property>
  </bean>

  <bean id="channelDecisionManager"
    class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
    <property name="channelProcessors">
      <list>
        <bean
          class="org.acegisecurity.securechannel.SecureChannelProcessor" />
        <bean
          class="org.acegisecurity.securechannel.InsecureChannelProcessor" />
      </list>
    </property>
  </bean>

</beans>


Add your user manager

Define the bean userManager referenced in the userDetailsService. You have to write this by yourself or download my code and use it. It is a basic service implementation which invokes my UserDAO and RoleDAO to provide user services.

Important: This bean needs to implement the org.acegisecurity.userdetails.UserDetailsService.


Now add zk.xml

The following is to be added to the zk.xml so that this listener is active and it copies the servlet thread ThreadLocal securityContext over to event thread ThreadLocal.

<zk>
  <listener>
    <description>Acegi SecurityContext Handler</description>
    <listener-class>
      org.zkoss.zkplus.acegi.AcegiSecurityContextListener
    </listener-class>
  </listener>
</zk>


Login status Window

Let us create a Login Status Window which we can use in all our pages. Depending on your setup, you may have to replace the UserInfo and its methods with an appropriate object that holds you user information.

package com.jinifed.wisdom.ui;

import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.log4j.Logger;
import org.zkoss.zul.Button;
import org.zkoss.zul.Label;
import org.zkoss.zul.Space;
import org.zkoss.zul.Window;

import com.jinifed.wisdom.model.UserInfo;
import com.jinifed.wisdom.service.UserManager;

/**
 * 
 * @author Hari Gangadharan (hari dot gangadharan at gmail)
 */
public class LoginStatusWindow extends Window {
  static final long serialVersionUID = 1L;
  UserManager userManager;
  SecurityContext context;
  UserInfo loggedinUser;
  Logger log = Logger.getLogger(this.getClass());
  
  
  public void onCreate() {
    context = SecurityContextHolder.getContext();
    Authentication authen = context.getAuthentication();
    Object principal = null;
    if (authen != null) {
      principal = authen.getPrincipal();
    }
    log.info("authen is " + authen);
    if (principal != null && principal instanceof UserInfo) {
      loggedinUser = (UserInfo) principal;
    }
    log.info("principal is " + principal);
    Label uiLablStatus = new Label();
    Space uiSpacSpace = new Space();
    uiSpacSpace.setBar(true);
    Button uiBttnLoginLogout = new Button();
    if (loggedinUser != null) {
      uiLablStatus.setValue("Logged in as " 
        + loggedinUser.getDisplayName() + " [" 
        + loggedinUser.getUsername() + "]");
      uiBttnLoginLogout.setLabel("Logout");
      uiBttnLoginLogout.setHref("logout.zul");
    } else {
      uiLablStatus.setValue("Not logged in");
      uiBttnLoginLogout.setLabel("Login / Register");
      uiBttnLoginLogout.setHref("login.zul");
    }
    this.appendChild(uiLablStatus);
    this.appendChild(uiSpacSpace);
    this.appendChild(uiBttnLoginLogout);
    
  }

}


Add a protected page and an unprotected page

Let us add the index.zul and link some secure pages to it. See that I am using DelegatingVariableResolver in all my pages so that I can invoke spring beans directly from my zul pages. Not used now in this page but I will use it later.

<?xml version="1.0" encoding="utf-8"?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
<?page title="Welcome to Wisdom"?>

<window id="demo" style="margin: 15px;">
  <vbox width="100%">
    <window id="uiWndwLoginStatus" style="text-align: right;"
      border="none" width="600px"
      use="com.jinifed.wisdom.ui.LoginStatusWindow" />
    <window id="uiWndwWelcome" border="none" width="600px">
      <vbox>
        <html>
          <attribute name="content">
            <![CDATA[
        <h2>Welcome to Wisdom</h2>
        <p>Wisdom is a sample application written to demonstrate Acegi and ZK.</p>
      ]]>
          </attribute>
        </html>
        <button label="Go to a secure page" href="createPost.zul" />
      </vbox>
    </window>
  </vbox>
</window>

WisdomHome.PNG

Since I have referred the createPost.zul, I have to create it also. This will be a protected page since /create* is a pattern that requires user or admin role (defined in security.xml).

<?xml version="1.0" encoding="utf-8"?>
<?page title="A Secure page in Wisdom"?>

<window id="demo" style="margin: 15px;">
  <vbox width="100%">
    <window id="uiWndwLoginStatus" style="text-align: right;"
      border="none" width="600px"
      use="com.jinifed.wisdom.ui.LoginStatusWindow" />
    <window id="uiWndwSecure" border="none"  width="600px">
      <vbox>
        <html>
          <attribute name="content">
            <![CDATA[
        <h2>A Secure page in Wisdom</h2>
        <p>This is a secure page in Wisdom. You have to login to see this page.</p>
      ]]>
          </attribute>
        </html>
        <button label="Go to home" href="/" />
      </vbox>
    </window>
  </vbox>
</window>


Login/ register page

Now it is time to add a login/register page. Use the following one for now. You can update it as needed.

<?xml version="1.0" encoding="utf-8"?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
<?page title="Wisdom Login"?>

<window id="demo" style="margin: 15px;">
  <vbox width="100%">
    <html>
      <attribute name="content">
        <![CDATA[
        <h2>Login to Wisdom</h2>
        <p>Login to Wisdom or signup for a new account. 
        The signup takes less than a minute for most users.</p>
      ]]>
      </attribute>
    </html>
    <tabbox width="400px">
      <tabs>
        <tab label="Login to Wisdom" />
        <tab label="Signup for new Account" />
      </tabs>
      <tabpanels>
        <tabpanel>
          <window id="uiWndwLogin" border="none" width="400px">
            <zscript>
            errorCode = Executions.getCurrent().getParameter("error");
            errorMessage = "";
            messageStyle="color: red; font-weight: bold;";
            if (errorCode == null) {
              errorMessage = "Enter your user name and password";
              messageStyle="color: green; font-weight: bold;";
            } else if ("1".equals(errorCode)) {
              errorMessage = "User name or password is incorrect";
            } else if ("2".equals(errorCode)) {
              errorMessage = "You need to login before continuing";
            } else {
              errorMessage = "Unknown error occured - "
               + "try again later or contact webmaster";
            }
            </zscript>
            <h:form id="loginForm" action="j_security_check"
              xmlns:h="http://www.w3.org/1999/xhtml">
              <label id="uiLablMessage" style="" 
                value="" />
              <grid>
                <rows>
                  <row>
                    Username:
                    <textbox id="uiTxtbUsername" name="j_username" 
                      value="" />
                  </row>
                  <row>
                    Password:
                    <textbox id="uiTxtbPassword" name="j_password"
                      type="password" constraint="no empty" />
                  </row>
                </rows>
              </grid>
              <vbox width="100%">
                <separator/>
                <checkbox label="Save my password" name="rememberMe" 
                  id="uiChkbRememberMe"/>
                <button label="Login" onClick="login()" />
              </vbox>
              <zscript>
                <![CDATA[
        void login() {
          //validate data
          org.zkoss.zk.ui.Sessions.getCurrent()
            .setAttribute("username",uiTxtbUsername.getValue());
          uiTxtbPassword.getValue();
          
          //submit the form
          org.zkoss.zk.ui.util.Clients.submitForm(loginForm);
        }
        ]]>
              </zscript>
            </h:form>
          </window>
        </tabpanel>
        <tabpanel>
          <window id="uiWndwRegister" border="none" width="400px" >
            <label id="uiLablMessage" style="color: red" />
            <grid>
              <rows>
                <row>
                  Username:
                  <vbox>
                  <textbox id="uiTxtbUsername" constraint="no empty"  />
                  <button label="Check if available" 
                    onClick="checkUserNameAction.execute(uiWndwRegister)"/>
                  </vbox>
                </row>
                <row>
                  Password:
                  <textbox id="uiTxtbPassword" type="password" 
                    constraint="no empty" />
                </row>
                <row>
                  Confirm Password:
                  <textbox id="uiTxtbConfirmPassword" type="password" 
                    constraint="no empty" />
                </row>
                <row>
                  Display Name:
                  <textbox id="uiTxtbDisplayName" />
                </row>
                <row>
                  Email Id:
                  <textbox id="uiTxtbEmailId" constraint="no empty" />
                </row>
              </rows>
            </grid>
            <vbox width="100%">
              <separator/>
              <button label="Register" 
                onClick="regAction.execute(uiWndwRegister)" />
            </vbox>
          </window>
        </tabpanel>
      </tabpanels>
    </tabbox>
  </vbox>
</window>

WisdomLoginOnSecurePageAccess.PNG

Here you will notice the following:

  1. I am doing a post of the loginForm to the j_security_check so that Acegi will intercept and authenticate the user. If the user name or password is incorrect, the user is thrown back to this page by Acegi Security interceptor
  2. I have used two controllers, regAction bean and checkUserNameAction bean. That means you have to add those two beans

Now add the controllers regAction bean and the checkUserNameAction bean. But before that I have created an interface for all actions in my application. This interface is aptly named as Action is as follows:

package com.jinifed.wisdom.actions;

import org.zkoss.zul.impl.XulElement;

/**
 * The interface for all actions.
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 *
 */
public interface Action {
  /**
   * @param element the XulElement on which the action is performed
   */
  public void execute(XulElement element);

}

As expected our Action beans will implement this interface. First the RegisterAction:

package com.jinifed.wisdom.actions;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import org.zkoss.zk.ui.Executions;
import org.zkoss.zul.Label;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Textbox;
import org.zkoss.zul.impl.XulElement;

import com.jinifed.wisdom.model.UserInfo;
import com.jinifed.wisdom.service.UserManager;

/**
 * This Action takes care of the user registration.
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 * @spring.bean id="registerAction" autowire="byName"
 */
public class RegisterAction implements Action {
  UserManager userManager;
  
  /**
   * This method handles the User registration 
   */
  public void execute(XulElement element) {
    Textbox uiTxtbConfirmPassword = 
      (Textbox) element.getFellow("uiTxtbConfirmPassword");
    
    Label uiLablMessage = (Label) element.getFellow("uiLablMessage");
    
    String username = ((Textbox) element.getFellow("uiTxtbUsername"))
      .getValue().trim();
    String password = ((Textbox) element.getFellow("uiTxtbPassword"))
      .getValue().trim();
    String displayName = ((Textbox) element.getFellow("uiTxtbDisplayName"))
      .getValue().trim();
    String emailId = ((Textbox) element.getFellow("uiTxtbEmailId"))
      .getValue().trim();
    if (displayName == null || displayName.equals("")) {
      displayName = username;
    }
    
    if (!password.equals(uiTxtbConfirmPassword.getValue())) {
      uiLablMessage.setValue("Confirm password should be same as password");
      uiTxtbConfirmPassword.focus();
      return;
    }
    
    if (!userManager.isUserNameAvailable(username)) {
      uiLablMessage.setValue("Selected user name not available - try another one");
      uiLablMessage.setStyle("color: red; font-weight: bold;");
      return;
    }
    
    // now since everything is fine add the user
    UserInfo user = new UserInfo();
    user.setPassword(password);
    user.setUsername(username);
    user.setDisplayName(username);
    user.setEmailId(emailId);
    
    // for now I am enabling my user
    user.setEnabled(true);
    try {
      userManager.addUser(user);
    } catch (Exception e) {
      uiLablMessage.setValue("Failure while registering the user - "
        + "try again later");
      return;
    }
    // if we reached here we are ok
    try {
      Messagebox.show("Congratulations! You are now a registered user of wisdom."
        + " Please wait while you are logged in automatically",
        "Registration Successful",Messagebox.OK,Messagebox.INFORMATION);
    } catch (InterruptedException e) {
      // ignore
    }
    
    // now send a redirect to the acegi login service
    try {
      Executions.getCurrent().sendRedirect("j_security_check?j_username=" 
        + URLEncoder.encode(username, "UTF-8") + "&j_password=" 
        + URLEncoder.encode(password, "UTF-8"));
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }

  }

  /**
   * @return the userManager
   */
  public UserManager getUserManager() {
    return userManager;
  }

  /**
   * @param userManager the userManager to set
   * @spring.property ref="userManager"
   */
  public void setUserManager(UserManager userManager) {
    this.userManager = userManager;
  }

}

Now the checkUserNameAction bean:

package com.jinifed.wisdom.actions;

import org.zkoss.zul.Label;
import org.zkoss.zul.Textbox;
import org.zkoss.zul.impl.XulElement;

import com.jinifed.wisdom.service.UserManager;

/**
 * This Action checks if the user name selected is available. This is a action
 * that does not require login and hence this class implements the Action
 * interface.
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 * @spring.bean id="checkUserNameAction" autowire="byName"
 */
public class CheckUserNameAction implements Action {
  UserManager userManager;
  
  /**
   * This method handles the check if available button action.
   */
  public void execute(XulElement element) {
    Label uiLablMessage = (Label) element.getFellow("uiLablMessage");
    Textbox uiTxtbUsername = (Textbox) element.getFellow("uiTxtbUsername");
    String username = uiTxtbUsername.getValue().trim();
    uiLablMessage.setStyle("color: green; font-weight: bold;");

    if (userManager.isUserNameAvailable(username)) {
      uiLablMessage.setValue("User name " + username + " is available");
    } else {
      uiLablMessage.setValue("User name " + username + " is not available - "
        + "try another one");
      uiLablMessage.setStyle("color: red; font-weight: bold;");
    }
  }

  /**
   * @return the userManager
   */
  public UserManager getUserManager() {
    return userManager;
  }

  /**
   * @param userManager the userManager to set
   * @spring.property ref="userManager"
   */
  public void setUserManager(UserManager userManager) {
    this.userManager = userManager;
  }

}

I have used the XDoclet Tags and created the spring XML bean definitions using the springdoclet task. If you plan to create the application context manually, you may have to create it following the XDoclet Tags in this code. In my userManager bean I have two methods isUserNameAvailable(UserInfo userInfo) and addUser(UserInfo userInfo). The above classes will have compile errors is your userManager bean does not have these methods. You can also change the method names to the equivalent name used in your bean.

WisdomRegister.PNG

WisdomRegisterSuccess.PNG


Let us test

Now we have reached a milestone… You can save everything, compile and try accessing the index.zul. You will be able to click on “Go to a secure page” button and you should be redirected to the Login/ Register page. After login (or after registration) you will be redirected to the secure page createPost.zul. With this (if this worked), it is possible to secure zul pages but we have still not implemented the action security. For example, if a user clicks on a button, then the user should be redirected to the login page if that action required login.

WisdomSecurePage.PNG


Creating Actions that are protected



Create a Sample Protected Action and Protected Action Interface

Let us create a protected action to demonstrate this. For that we need an interface to mark the protected actions. I used ProtectedAction interface for that: This is a dummy interface:

package com.jinifed.wisdom.actions;

/**
 * A dummy interface to mark an action as a protected action. Any action that
 * requires login should implement this interface.
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 */
public interface ProtectedAction extends Action {
  
}

Now I created the sample protected action:

package com.jinifed.wisdom.actions;

import org.zkoss.zul.Messagebox;
import org.zkoss.zul.impl.XulElement;

/**
 * This is a sample protected action. All actions that require login has 
 * to implement the ProtectedAction interface. To invoke this do the following:
 * 
 * var command = "sampleProtectedAction"; 
 * frontController.doAction(command,window)
 * 
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 * @spring.bean id="sampleProtectedAction" autowire="byName"
 */
public class SampleProtectedAction implements ProtectedAction {

  public void execute(XulElement element) {
    try {
      Messagebox.show("Congratulations! "
      +"You are authenticated user and now you can access this action",
      "Success",Messagebox.OK,Messagebox.EXCLAMATION);
    } catch (InterruptedException e) {
      // ignore
    }
  }

}

You will notice that I have implemented the ProtectedAction instead of the Action Interface. Once again you will have to create the bean definitions in the spring application context if you are not using the XDoclet.


The front controllers

Finally to protect the action, we need a Front controller and an Application Controller so that I can catch the exception generated by the spring method interceptor. Let us see how I have implemented this.

First the Front Controller:

package com.jinifed.wisdom.controller;

import org.acegisecurity.AccessDeniedException;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zkplus.spring.SpringUtil;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.impl.XulElement;

import com.jinifed.wisdom.actions.Action;
import com.jinifed.wisdom.actions.ProtectedAction;


/**
 * Front controller for the UI Actions.
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 * @spring.bean id="frontController" autowire="byName"
 */
public class FrontController {
  ApplicationController controller;
  ApplicationController protectedController;
  String loginPage;
    
  /**
   * Executes an action. If the action is a protected action then the
   * Action is invoked through a proxy bean secured by Acegi method
   * interceptor.
   * 
   * @param actionName  the action to invoke
   * @param element   the element on which the action is invoked
   */
  public void doAction(String actionName, XulElement element) {
    Action action = (Action) SpringUtil.getBean(actionName);
    
    if (action instanceof ProtectedAction) {
      try {
        protectedController.execute(action, element);
      } catch (AccessDeniedException e) {
        try {
          Messagebox.show(
          "You should be an authenticated user to use this action."
          +" You will be redirected to the login/register screen.", 
          "Authentication Required",Messagebox.OK,Messagebox.ERROR);
        } catch (InterruptedException e1) {
          // ignore
        }
        Executions.sendRedirect(loginPage);
      }     
    } else {
      controller.execute(action, element);      
    }
  }
  
  /**
   * @return the controller
   */
  public ApplicationController getController() {
    return controller;
  }
  /**
   * @param controller the controller to set
   * @spring.property ref="applicationController" dependency-check="none"
   */
  public void setController(ApplicationController controller) {
    this.controller = controller;
  }
  /**
   * @return the protectedController
   */
  public ApplicationController getProtectedController() {
    return protectedController;
  }
  /**
   * @param protectedController the protectedController to set
   * @spring.property ref="protectedController" dependency-check="none"
   */
  public void setProtectedController(ApplicationController protectedController) {
    this.protectedController = protectedController;
  }

  /**
   * @return the loginPage
   */
  public String getLoginPage() {
    return loginPage;
  }

  /**
   * @param loginPage the loginPage to set
   * @spring.property value="/login.zul?error=2"
   */
  public void setLoginPage(String loginPage) {
    this.loginPage = loginPage;
  }
}

Now the application controller:

package com.jinifed.wisdom.controller;

import org.acegisecurity.AccessDeniedException;
import org.apache.log4j.Logger;
import org.zkoss.zul.impl.XulElement;

import com.jinifed.wisdom.actions.Action;

/**
 * Application controller
 * 
 * @author Hari.Gangadharan (hari dot gangadharan at gmail dot com)
 * @spring.bean id="applicationController" autowire="byName"
 */
public class ApplicationController {
  Logger log = Logger.getLogger(this.getClass());

  public void execute(Action action, XulElement element) 
  throws AccessDeniedException {
    action.execute(element);
  }
}

Some of you may get my plan – I am planning to wrap the ApplicationController in a proxy that uses Acegi method interceptor. I maintain reference to the instance of the proxy and the instance of the application controller in the front controller. If the action that is getting executed is an instance of ProtectedAction then the front controller uses the proxy; otherwise it uses the instance of ApplicationController directly.


Add proxy definition on security.xml

For this all to work, some more definitions are to be added to the security.xml:

<!-- A transaction Proxy template -->
<bean id="txProxyTemplate" abstract="true"
  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  <property name="transactionManager" ref="transactionManager"/>
  <property name="transactionAttributes">
    <props>
      <prop key="*">PROPAGATION_REQUIRED</prop>
    </props>
  </property>
</bean>


<!-- A proxy which uses Acegi Method Security Interceptor -->
<bean id="protectedController" parent="txProxyTemplate">
  <property name="target" ref="applicationController" />
  <property name="preInterceptors">
    <list>
      <ref bean="protectedControllerSecurity"/>
    </list>
  </property>
</bean>

<bean id="protectedControllerSecurity"
  class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
  <property name="authenticationManager" ref="authenticationManager"/>
  <property name="accessDecisionManager" ref="accessDecisionManager"/>
  <property name="objectDefinitionSource">
    <value>
      com.jinifed.wisdom.controller.ApplicationController.execute=user
    </value>
  </property>
</bean>

Finally let us add a protected action in the index page. For this add the following lines after the button in the index.zul:

<zscript>
  var action = "sampleProtectedAction";
</zscript>
<button label="A Secure Action" 
  onClick="frontController.doAction(action,uiWndwWelcome)"/>


Update other actions to use front controller (optional)

Notice that I am invoking frontController bean. For all actions, I would recommend that you invoke the frontController instead of invoking the bean directly. If the action implements ProtectedAction then Acegi method interceptor will intercept the call since the front controller invokes the proxy’s execute() method. For this you have to update the actions in the login.zul:

Replace “Register” button with the following code:

<zscript>
  var regAction = "registerAction";
</zscript>
<vbox width="100%">
  <separator/>
  <button label="Register" 
    onClick="frontController.doAction(regAction,uiWndwRegister)" />
</vbox>

Replace the “Check if available” button with the following code:

<zscript>
  var checkAction = "checkUserNameAction";
</zscript>
<button label="Check if available" 
  onClick="frontController.doAction(checkAction,uiWndwRegister)"/>


Ready to test

Now you should be able to test the protected action by clicking the “A Secure Action” button in index.zul. You will be shown an “Authentication Required” message box and you will be redirected to the Login/Register page.

WisdomSecureAction1.PNG

WisdomSecureAction2.PNG


Conclusion

I admit that my implementation of securing actions is not very flexible since you cannot easily configure roles for each action. For example if you need an action that can be accessed only by admin then the only way to do that would be to create a new proxy which requires admin role. Also you have to create a new dummy interface for actions that requires admin role. Currently I can’t think of an easy way to do that but I will work on it. Please direct all comments and suggestions to me (hari dot gangadharan at gmail dot com).

Ruler.gif


Disclaimer

I have read / used ideas presented in many sites -- too numerous so that I cannot list all of them. However I have to thank the following authors or projects:

  1. Bart van Riel - Acegi tutorial http://www.tfo-eservices.eu/wb_tutorials/pages/spring-acegi-tutorial.php
  2. Colin’s blog http://blog.exis.com/colin
  3. AppFuse project https://appfuse.dev.java.net
  4. Sanjiv Jivan's blog http://jroller.com/page/sjivan?entry=ajax_based_login_using_aceci


I would recommend my readers to read these articles also.

Ruler.gif

Download the example code here.


  • This is for the eclipse project.
  • You need to add Spring, Acegi jar, hibernate, your database driver jar, ZK jars and all dependencies.
  • I have included the jars like Xdoclet needed to generate some files – see build_tools folder.
Ruler.gif

Hari Gangadharan is a software architect working for Charles Schwab and Co. (San Francisco Bay Area, USA). Hari focuses on Java Webservices and batch Java applications and has used spring and hibernate extensively. He is also an AJAX enthusiast.




Copyright © Hari Gangadharan. This article is licensed under GNU Free Documentation License.