Sesja i zmiana layoutu w ProgramBash

Po rejestracji/zalogowaniu w jakimkolwiek serwisie internetowym w zdecydowanej większości przypadków zmianie ulega layout strony. Czasem prawie wcale, jednakże najczęściej pojawia się całkiem sporo nowych rzeczy – pasek z linkami, informacje o sesji i różne takie. Nie ma w tym nic zdrożnego zatem i w przypadku ProgramBash wypadałoby zrobić coś podobnego.

Prace zakończyliśmy w momencie, w którym rejestrujemy nowego użytkownika i automatycznie jest on logowany do serwisu. Jak również pisałem poprzednio po fakcie rejestracji jesteśmy przerzucani (póki co) na stronę główną z radosnym komunikatem o powodzeniu operacji. Jednak poza tym komunikatem, absolutnie nic nie pokazuje nam, że jesteśmy zalogowani, zaś o takich detalach jak login choćby nie wspomnę. Czyli musimy pobawić się w logikę widoku.

Dla przypomnienia – w przypadku ProgramBash warstwą widoku rządzi biblioteka Apache Tiles. Przypomnieć również wypada, iż obecnie istniejący layout został zdefiniowany w taki oto sposób (plik faces-config.xml):

<tiles-definitions>
    <definition name="guestLayout" template="/WEB-INF/layout/guestLayout.jsp">
   
         <put-attribute name="upperMenu" value="/WEB-INF/layout/upperMenuTemplate.jsp" />
         <put-attribute name="content" />
         <put-attribute name="footer" value="/WEB-INF/layout/footerTemplate.jsp" />

    </definition>
</tiles-definitions>

Nazwy mówią za siebie – domyślnie mamy do czynienia z guestLayout, czyli widokiem przeznaczonym dla niezalogowanych użytkowników. Zadanie jest proste – wyświetlić kilka dodatkowych informacji bazując na fakcie, iż jesteśmy zalogowani. I tutaj zasadniczo problem się rozwiązał, gdyż na dłuższą metę rzecz w 2 instrukcjach warunkowych, które umieszczone w widoku (konkretniej w kafelku z menu) będą określały czy coś pokazać, czy nie. Dlatego też ten zapis pozostanie niezmieniony, zaś przegrzebiemy kilka rzeczy w odpowiednim pliku JSP, a konkretnie upperMenuTemplate.jsp. Wyglądać on będzie po tej operacji tak:

 <div id="menu">
                <ul>
                    <li><a href="/glowna.faces" title="home">GŁÓWNA</a></li>
                    <li><a href="/top" title="top">TOP</a></li>
                    <li><a href="/szukajka" title="Szukaj">SZUKAJ</a></li>

                     <h:panelGroup rendered="#{userController.logged == true}">
                         <li><a href="/dodajwpis" title="Dodaj wpis">DODAJ WPIS</a></li>
                         <li><a href="/logout.faces" title="Wyloguj">WYLOGUJ</a></li>
                    </h:panelGroup>

                    <h:panelGroup rendered="#{userController.logged == false}">
                        <li><a href="/rejestruj.faces" title="Rejestracja">REJESTRACJA</a></li>
                        <li><a href="/login.faces" title="Zaloguj">ZALOGUJ</a></li>
                    </h:panelGroup>
                    
                    <li>
                        <a href="#" onclick="document.getElementById('mp').component.show(); return false;" tabindex="0">O AUTORZE</a>
                    </li>
                    <li><a href="mailto:michpio@gazeta.pl" title="Contact">KONTAKT</a></li>

                     <h:panelGroup rendered="#{userController.logged}">
                         <li>
                             <h:form>
                                 Zalogowany jako: <strong><h:commandLink action="#{userController.showUser}" ><h:outputText value="#{user.nick}" /></h:commandLink></strong>
                                 <h:inputHidden id="userId" value="#{user.id}" />
                             </h:form>
                         </li>
                    </h:panelGroup>
                </ul>
        </div>

Jak widać do klasy UserController dodano własność logged wraz ze stosownymi metodami dostępowymi. Rzecz jest dość prosta. Bardziej skomplikowaną rzeczą jest stworzenie czegoś na kształt ACLa, który pilnowałby użytkownika przed wejściem pod wskazane adresy. Skoro bowiem ktoś jest już zalogowany, to po co miałby wchodzić ponownie na ekran rejestracji? Takie sprawdzenie pewnych warunków (konkretniej zalogowania, przynajmniej na razie) musi odbywać się za każdym żądaniem skierowanym do aplikacji. Tutaj przydałby się nam znów jakiś phaseListener. Jednakże by ułatwić sobie pracę wpierw dodamy możliwość zalogowania się oraz wylogowania – bez konieczności rejestrowania się w serwisie za każdym razem. Dodamy zatem widok związany z logowaniem. Oto jak powinien wyglądać:

<%@taglib uri="http://richfaces.org/a4j" prefix="a4j"%>
<%@taglib uri="http://richfaces.org/rich" prefix="rich"%>

<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>

<%@taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<div class="form-container">
  
    <h:form id="registrationForm" prependId="false">

	<fieldset>
		<legend>Zaloguj się do serwisu</legend>
		
			<div>
                            <label for="email">E-mail</label>
                            <h:inputText value="#{user.email}" id="email" required="true"  requiredMessage="#{msgs.emailRequired}">
                                <f:validateLength maximum="256" />
                                <f:validator validatorId="com.wordpress.chlebik.Email" />
                            </h:inputText>
                            <h:message for="email" errorClass="error"/>
                        </div>

                        <div>
                            <label for="pass">Hasło</label>
                            <h:inputSecret value="#{user.pass}" id="pass"  required="true"  requiredMessage="#{msgs.passRequired}">
                                 <f:validateLength maximum="30" minimum="5" />
                            </h:inputSecret>
                            <h:message for="pass" errorClass="error"/>
                        </div>
              
	</fieldset> 

	<div class="buttonrow">
            <h:commandButton type="submit" value="Zaloguj" id="login" immediate="false" action="#{userController.login}"  />
	</div>

        </h:form>

</div>

Jak widać rzecz jest prosta jak konstrukcja snopowiązałki. 2 pola, pod które podpięto walidatory i akcja to wywołania. Akcję wrzucamy do naszej starej i znajomej klasy userController. Dzięki temu część kodu będzie można pewnikiem zrefaktoryzować i sprawić by służył zarówno rejestracji jak i logowaniu. Kod akcji prezentuje taki oto listing:

 /**
    * Akcja obslugujaca logowanie w systemie
    *
    * @return String
    * @author Michał Piotrowski
    */
    public String login() {

        FacesContext context = FacesContext.getCurrentInstance();
        Map params = context.getExternalContext().getRequestParameterMap();
       
        String pass       =    (String)  params.get("pass");
        String email      =    (String)  params.get("email");

         SessionFactory sessionFactory = ProgramBashUtil.getSessionFactory();
         Session session = sessionFactory.openSession();
         String toReturn  =   "login-failed";

        try {
               Query zapytanie = session.createQuery( "FROM User WHERE email = '" + email + "' AND pass = '" + MD5.MD5( pass ) + "'" );
               User existingUser = (User) zapytanie.uniqueResult();

               if( existingUser instanceof User ) {
                   context.getExternalContext().getSessionMap().put("user", existingUser );
                   logged = true;
                   context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_INFO, "Logowanie powiodło się", "") );
                   toReturn = "login-success";
               } else {
                   context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Podano złe dane!", "") );
                   toReturn = "login-failure";
               }
    
               return toReturn;

        } catch( Exception e ) {
            context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Logowanie nie powiodło się!", "") );
            return "login-failure";
        } finally {
            session.close();
        }
    }

I takie oto działanie podpięte zostaje pod wywołanie widoku /login.faces. Możemy się zatem spokojnie zalogować. Efektem będzie taki oto choćby widok:

Wylogowanie z kolei podpięliśmy pod stosowny link w górnym menu. Kod w kontrolerze użytkownika jest prościutki:

 /**
     * Metoda sluzaca do wylogowania z serwisu
     *
     * @return String
     */
    public String logout() {

         FacesContext context = FacesContext.getCurrentInstance();
         context.getExternalContext().getSessionMap().put( "user", null );
         logged = false;
         context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_INFO, "Poprawnie wylogowano Cię z serwisu!", "") );
         return null;
    }

Teraz kiedy już mamy możliwość zalogowania się i wylogowania kiedy tylko chcemy możemy przystąpić do implementacji czegoś na kształt ACLa. Jak już wspomniałem wyżej potrzebny nam będzie do tego kolejny phaseListener, który będzie sprawdzał czy user jest zalogowany czy nie, a tym samym czy pewne akcje są dla niego dostępne, a które nie są.

package com.wordpress.chlebik.filters;

import com.wordpress.chlebik.controllers.UserController;
import java.util.Arrays;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

/**
 * Klasa obslugujaca ACL
 *
 * @author Michał Piotrowski
 */
public class ACLHandler implements PhaseListener {

    String[] loggedNotAllowed    =   new String[]{ "/login.jsp", "/rejestruj.jsp" };
    String[] loggedNeeded        =   new String[]{};

    public PhaseId getPhaseId() {
         return PhaseId.ANY_PHASE;
    }

    @Override
     public void beforePhase(PhaseEvent e) {

        if( e == null ) {
            return;
        }

        if( e.getPhaseId()== PhaseId.RENDER_RESPONSE )
        {
            FacesContext facesContext = e.getFacesContext();

            HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
            UserController controller = (UserController) session.getAttribute( "userController" );
            String viewId = facesContext.getViewRoot().getViewId();
            Arrays.sort( loggedNotAllowed );
            Arrays.sort( loggedNeeded );

            if( controller != null && controller.isLogged() ) {

                int viewIndex = Arrays.binarySearch( loggedNotAllowed, viewId );

                if( viewIndex > 0 ) {
                    facesContext.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Musisz się wylogować by móc skorzystać z tej opcji!", "") );
                    facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, "", "logoutNeeded" );
                }

            }  else {

                int viewIndex = Arrays.binarySearch( loggedNeeded, viewId );

                if( viewIndex > 0 ) {
                    facesContext.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Musisz się zalogować by móc skorzystać z tej opcji!", "") );
                    facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, "", "loginNeeded" );
                }
            }
        }
    }

    @Override
    public void afterPhase(PhaseEvent event) { }

}

Rzecz jasna klasę należy dorzucić do konfiguracji. Modyfikujemy zatem plik faces-config.xml:

 <lifecycle>
        <phase-listener> com.wordpress.chlebik.filters.ACLHandler</phase-listener>
 </lifecycle>

I tym samym mamy śliczny pseudo-ACL. Z premedytacją użyłem przedrostka “pseudo”, gdyż w duzych i skalowalnych aplikacjach ACL opiera się najczęściej na rolach/grupach przywilejów, często z możliwością ich modyfikacji, zaś to wszystko wyciągane z bazy danych i równie często trzymane w cache. Jednakże na potrzeby naszej aplikacji powyższe rozwiązanie działa bardzo dobrze – zasadniczo niewiele rzeczy (tak planuję) będzie wymagało zalogowania/wylogowania. Grunt, iż podane rozwiązanie działa.

Advertisements

2 thoughts on “Sesja i zmiana layoutu w ProgramBash

  1. chlebik Post author

    A to dlatego, ze w sekcji TRY musialbym dawac return 2 razy (w zaleznosci od warunku) wiec dorzucilem zmienna String by istniala w bloku tylko jedna instrukcja RETURN.

    Przy lapaniu wyjatku mozna zwrocic tylko stringa z niepowodzeniem wiec stad wklepalem string bezposrednio.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s