понедельник, 6 мая 2013 г.

Часть 1. Spring Roo + JSF + Spring Security: Регистрация нового пользователя и reCaptcha

English translation of this post is here.
Пост будет полезен людям, начинающим осваивать Spring Roo. Я новичок, поэтому буду рад услышать ваши комментарии, замечания, и суровое мнение профессионалов.

Задача регистрации нового пользователя стандартна для подавляющего большинства приложений. При этом, как в текущей версии Roo, так и в "родительском" Spring Security, присутствуют стандартные формы входа в систему, а вот регистрация пользователя обходится стороной, в том числе и в большинстве учебных материалов.

Кроме того, команда включения в проект Spring Security (security setup) принудительно отключается в Roo, если выбирается генерация интерфейса JSF (web jsf setup), а не MVC. Настройки безопасности мне еще предстоят, поэтому расскажу о них в следующем посте, если этот вызовет достаточный интерес у публики. А пока остановлюсь на пользовательском интерфейсе.

Зачем же мучиться с JSF если частично есть стандартизованный механизм для MVC? Да просто интерфейс PrimeFaces мне нравится больше.

Я использую 64-битную STS 3.2.0 под Ubuntu 12.04. Скрипт проекта выглядит так:

project --topLevelPackage org.test.sec2 --projectName test.sec2 --java 7 --packaging JAR
jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY
entity jpa --class ~.domain.Person --activeRecord false --equals --testAutomatically

field string --fieldName login --sizeMax 25 --notNull

field string --fieldName password --sizeMax 25 --notNull

repository jpa --interface ~.domain.PersonRepository --entity ~.domain.Person

web jsf setup --theme CASABLANCA

web jsf all --package ~.web
Если вы создаете проект в STS, первую команду система введет за вас, попросив у вас имя проекта и названия пакета. В этом случае начинайте с jpa setup в открывшейся консоли Roo для вашего нового проекта.

Если вы работаете непосредственно в командной строке Roo, запущенного в консоли, начинайте с самого начала.

В итоге получаем простое приложение с тестовой базой данных на движке Hipersonic, в оперативной памяти, доступом к СУБД с помощью ORM Hibernate, единственной таблицей Person с двумя полями - логин и пароль.

Теперь запустите проект и убедитесь что все работает. Попробуйте создать пользователей.

Когда вы заводили пользователей, то заметили, что:
1. Поле "пароль" вводится обычным текстом, а не звездочками.
2. Нет поля подтверждения пароля.
3. Хочется защиту от ботов - капчу.

Поэтому дорабатываем напильником.

1.Подготовительный этап - перенос метода public HtmlPanelGrid populateCreatePanel()


Если внимательно приглядеться к файлу src/main/webapp/pages/person.xhtml, становится понятно, что диалоговое окошко, с которым вы поиграли, создавая нового пользователя в своем приложении, генерируется динамически.

Дальше мы будем смотреть соответствующий файл,  сгенерированный Roo. Однако для удобства (и защиты от неквалифицированных программистов) разработчики STS заботливо скрыли от нас нужные файлы. Поэтому перед прочтением следующего абзаца, в Package Explorer нажмите направленный вниз серый треугольник, и в выпавшем меню уберите галочку Hide generated Spring Roo ITD's.

А теперь, присмотревшись к появившемуся файлу /test.sec2/src/main/java/org/test/sec2/web/PersonBean_Roo_ManagedBean.aj, становится понятно, что за начинку (внешний вид и оформление конкретных полей ввода) отвечает как раз метод public HtmlPanelGrid populateCreatePanel().

Грозное предупреждение в начале этого файла *.aj говорит нам о том, что
// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO. Потому что при обновлении проекта или даже версии Roo, он, Roo, изменит этот файл как угодно без зазрения совести, уничтожив все плоды ваших трудов.

Поэтому перенесем метод в сам класс /test.sec2/src/main/java/org/test/sec2/web/PersonBean.java. Для этого в окне Outline раскрываем файл PersonBean_Roo_ManagedBean.aj, щелкаем правой кнопкой на методе populateCreatePanel и выбираем Refactor->Push In..., затем указываем наш класс PersonBean.java.

Откройте PersonBean.java, он должен выглядеть примерно так:
package org.test.sec2.web;

import javax.el.ELContext;
...

@RooSerializable
@RooJsfManagedBean(entity = Person.class, beanName = "personBean")
public class PersonBean {

    public HtmlPanelGrid populateCreatePanel() {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application application = facesContext.getApplication();
        ExpressionFactory expressionFactory = application.getExpressionFactory();
        ELContext elContext = facesContext.getELContext();
       
        HtmlPanelGrid htmlPanelGrid = (HtmlPanelGrid) application.createComponent(HtmlPanelGrid.COMPONENT_TYPE);
         .......
2. "Звездочки" и совпадение пароля с подтверждением

Проанализируем код метода populateCreatePanel. Для каждого поля сущности Person создаются три элемента: заголовок OutputLabel, поле ввода InputText и всплывающее окошко сообщения об ошибке Message.

А также примем во внимание, что, спасибо разработчикам Primefaces, у них есть стандартный тег пароля со звездочками, и задача сравнения паролей ими решена, достаточно лишь указать параметр match в теге первого поля ввода пароля.

Поэтому вставляем следующий код вместо трех "абзацев" кода для поля ввода пароля:

OutputLabel passwordCreateOutput = (OutputLabel) application.createComponent(OutputLabel.COMPONENT_TYPE);
        passwordCreateOutput.setFor("passwordCreateInput");
        passwordCreateOutput.setId("passwordCreateOutput");
        passwordCreateOutput.setValue("Password:");
        htmlPanelGrid.getChildren().add(passwordCreateOutput);
               
        Password passwordCreateInput = (Password) application.createComponent(Password.COMPONENT_TYPE);
        passwordCreateInput.setId("passwordCreateInput");
        passwordCreateInput.setValueExpression("value", expressionFactory.createValueExpression(elContext, "#{personBean.person.password}", String.class));
        LengthValidator passwordCreateInputValidator = new LengthValidator();
        passwordCreateInputValidator.setMaximum(25);
        passwordCreateInput.addValidator(passwordCreateInputValidator);
        passwordCreateInput.setRequired(true);
        passwordCreateInput.setMatch("passwordCreateInput2");
        htmlPanelGrid.getChildren().add(passwordCreateInput);
       
        Message passwordCreateInputMessage = (Message) application.createComponent(Message.COMPONENT_TYPE);
        passwordCreateInputMessage.setId("passwordCreateInputMessage");
        passwordCreateInputMessage.setFor("passwordCreateInput");
        passwordCreateInputMessage.setDisplay("icon");
        htmlPanelGrid.getChildren().add(passwordCreateInputMessage);
       
        OutputLabel passwordCreateOutput2 = (OutputLabel) application.createComponent(OutputLabel.COMPONENT_TYPE);
        passwordCreateOutput2.setFor("passwordCreateInput2");
        passwordCreateOutput2.setId("passwordCreateOutput2");
        passwordCreateOutput2.setValue("Password Again:");
        htmlPanelGrid.getChildren().add(passwordCreateOutput2);
       
        Password passwordCreateInput2 = (Password) application.createComponent(Password.COMPONENT_TYPE);
        passwordCreateInput2.setId("passwordCreateInput2");
        passwordCreateInput2.setValueExpression("value", expressionFactory.createValueExpression(elContext, "#{personBean.person.password}", String.class));
        LengthValidator passwordCreateInputValidator2 = new LengthValidator();
        passwordCreateInputValidator2.setMaximum(25);
        passwordCreateInput2.addValidator(passwordCreateInputValidator);
        passwordCreateInput2.setRequired(true);
        htmlPanelGrid.getChildren().add(passwordCreateInput2);
       
        Message passwordCreateInputMessage2 = (Message) application.createComponent(Message.COMPONENT_TYPE);
        passwordCreateInputMessage2.setId("passwordCreateInputMessage2");
        passwordCreateInputMessage2.setFor("passwordCreateInput2");
        passwordCreateInputMessage2.setDisplay("icon");
        htmlPanelGrid.getChildren().add(passwordCreateInputMessage2);
Сохранитесь и обновите страничку, проверьте, что вы получаете сообщение об ошибке при несовпадении паролей.

3. Капча

Здесь нам тоже надо благодарить Primefaces. Есть тег для стандарта де-факто - reCaptcha от Google.

Для работы reCaptcha вам надо получить публичный и частный ключи, зарегистрировавшись на Google и указав сервер localhost.

Прописываем их в файл /test.sec2/src/main/webapp/WEB-INF/web.xml:

    <context-param>
        <param-name>primefaces.PRIVATE_CAPTCHA_KEY</param-name>
        <param-value>ваш частный ключ</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.PUBLIC_CAPTCHA_KEY</param-name>
        <param-value>ваш публичный ключ</param-value>
    </context-param>

Теперь немного шаманства. Найдите диалог createForm в файле /test.sec2/src/main/webapp/pages/person.xhtml и удалите параметр dynamic="true" в теге <p:dialog>. Без этого капча не работает, великие гуру объяснили это тем, что p:dialog - это pain in ass (заноза в заднице, простите за неточный перевод).

После шаманства добавляем стандартным тегом <p:captcha ....> слегка украшенную капчу . Дабы сообщение об ошибке было вменяемым, добавляем параметр label, иначе оно будет приходить от j_026, что заставит пользователя крепко призадуматься.

В итоге, диалог должен выглядеть так:

<p:dialog id="createDialog" header="#{messages.label_create} Person" modal="true" widgetVar="createDialogWidget" dynamic="true" visible="#{personBean.createDialogVisible}" resizable="true" maximizable="true" showEffect="fade" hideEffect="explode">
      <p:ajax event="close" update=":dataForm:data" listener="#{personBean.handleDialogClose}" />
      <p:outputPanel id="createPanel">
        <h:form id="createForm" enctype="multipart/form-data">
          <h:panelGrid id="createPanelGrid" columns="3" binding="#{personBean.createPanelGrid}" styleClass="dialog" columnClasses="col1,col2,col3" />
         
          <p:captcha label="reCaptcha" theme="white"/>
         
          <p:commandButton id="createSaveButton" value="#{messages.label_save}" action="#{personBean.persist}" update="createPanelGrid :growlForm:growl" />
          <p:commandButton id="createCloseButton" value="#{messages.label_close}" onclick="createDialogWidget.hide()" type="button" />
        </h:form>
      </p:outputPanel>
    </p:dialog>


Вуаля

Комментариев нет:

Отправить комментарий