Pull to refresh

OpenShift: «hello, cloud!»

Reading time13 min
Views13K
Это продолжение заметки про использование OpenShift в качестве java-хостинга.
В прошлый раз мы разобрались как создавать приложения в облаке OpenShift. В наше распоряжение предоставлен бесплатный хостинг с сервером JBoss AS 7.1 и репозиторием git. Теперь попробуем написать что-нибудь чуть сложнее, чем обычный «hello, world», и использующее возможности JBoss AS и средств разработки JBoss Tools.


Одна из распространенных задач: разрешить доступ к определенным ресурсам только авторизованным пользователям, с разделением в соответствии с присвоенными ролями. Предлагается сделать это с использованием встроенного в jboss логин-модуля, а именно реализацией org.jboss.security.auth.spi.DatabaseServerLoginModule. Как не трудно догадаться, в этом случае пользователи и их роли будут храниться в базе данных.

Схема данных достаточно проста: это таблица APP_USER (пользователи), APP_ROLE (справочник ролей) и APP_MEMBERSHIP (назначенные роли), через которую реализуется связь много-ко-многим между первыми двумя таблицами.


Создадим в web-консоли новое jbossas-7 приложение с картриджем mysql-5.1 и импортируем его в Eclipse. Следует переключиться в перспективу «Web». Сразу после импорта, скорее всего, раздел Java Resources будет помечен как содержащий ошибку, а окне Problems будет написана причина:
Project configuration is not up-to-date with pom.xml. Run project configuration update
Последуем данному совету: выделяем корень проекта, вызываем контекстное меню Maven -> Update Project Configuration, выполняем, и ошибка исчезнет.

Развернем дерево проекта:

Как видно, тут уже есть папки для java классов и ресурсов, а также в папке webapp файлы index.html, пара jsp-файлов, каталог WEB-INF с дескрипторами. Файл health.jsp можно сразу же удалить (а также описание сервлета health из дескриптора web.xml), зачем он здесь — непонятно. Файл snoop.jsp еще может пригодиться, в нем выводится кое-какая статистика о нашем приложении.
В корне проекта лежит pom.xml с единственной зависимостью
<dependency>
	<groupId>org.jboss.spec</groupId>
	<artifactId>jboss-javaee-6.0</artifactId>
	<version>1.0.0.Final</version>
	<type>pom</type>
	<scope>provided</scope>
</dependency>

Это дает нам доступ ко всем включенным в jboss модулям (ознакомиться со всем списком можно, развернув ветку Libraries — Maven Dependencies.

Настройка конфигурации сервера


Теперь нам понадобится файл, который не был импортирован Eclipse. Он находится в каталоге проекта по адресу .openshift/config/standalone.xml, и, как видно из названия, описывает конфигурацию экземпляра сервера jboss. Откроем его тут же, в Eclipse (если приложение будет отлаживаться на локальном сервере jboss, придется подобные манипуляции выполнить с файлом в папке сервера standalone/configuration/standalone.xml).

Настройка кодировки

Для работы с русскими символами в базе данных соединение должно осуществляться в кодировке UTF-8. Поэтому найдем источник данных (в данном случае MysqlDS) и добавим сведения о кодировке:
<connection-url>jdbc:mysql://${env.OPENSHIFT_DB_HOST}:${env.OPENSHIFT_DB_PORT}/${env.OPENSHIFT_GEAR_NAME}?characterEncoding=UTF-8</connection-url>


Настройка модуля аутентификации

Теперь создадим домен безопасности, который назовем, например «app-auth». Необходимо найти подсистему «urn:jboss:domain:security:1.1» и добавить в нее описание нашего домена:
<security-domain name="app-auth">
    <authentication>
        <login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule" flag="required">
            <module-option name="dsJndiName" value="java:jboss/datasources/MysqlDS"/>
            <module-option name="principalsQuery" value="select PWD from APP_USER where USER_NAME=? and ENABLED=1"/>
            <module-option name="rolesQuery" value="select r.ROLE_NAME, 'Roles' from  APP_ROLE r, APP_MEMBERSHIP m, APP_USER u where r.ROLE_ID=m.ROLE_ID and m.USER_ID=u.USER_ID and u.USER_NAME=?"/>
            <module-option name="hashAlgorithm" value="SHA-1"/>
            <module-option name="hashEncoding" value="base64"/>
        </login-module>
    </authentication>
</security-domain>

Назначение свойств dsJndiName, principalsQuery, rolesQuery, думаю, очевидно. Последние 2 свойства говорят о том, что в базе будут храниться хеши паролей. Если эти свойства убрать, то пароли должны будут сохраняться в открытом виде, что допустимо при отладке, но с реальными данными делать не стоит.

Настройка приложения: Faces, безопасность, инициализация


добавим в web.xml следующие строки:
	<!-- JSF mapping -->
	<servlet>
		<servlet-name>Faces Servlet</servlet-name>
		<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.xhtml</url-pattern>
	</servlet-mapping>
	
	<!-- security -->
	<login-config>
		<auth-method>FORM</auth-method>
		<realm-name>app-auth</realm-name>
		<form-login-config>
			<form-login-page>/login.xhtml</form-login-page>
			<form-error-page>/login.xhtml</form-error-page>
		</form-login-config>
	</login-config>
	<security-role>
		<role-name>Admin</role-name>
	</security-role>
	<security-role>
		<role-name>Manager</role-name>
	</security-role>

	<security-constraint>
		<web-resource-collection>
			<web-resource-name>Admin Part</web-resource-name>
			<url-pattern>/admin/*</url-pattern>
			<http-method>GET</http-method>
			<http-method>POST</http-method>
		</web-resource-collection>
		<auth-constraint>
			<role-name>Admin</role-name>
		</auth-constraint>
	</security-constraint>
	<security-constraint>
		<web-resource-collection>
			<web-resource-name>All Users</web-resource-name>
			<url-pattern>/view/*</url-pattern>
		</web-resource-collection>
		<auth-constraint>
			<role-name>*</role-name>
		</auth-constraint>
		<user-data-constraint>
			<transport-guarantee>NONE</transport-guarantee>
		</user-data-constraint>
	</security-constraint>
	
	<!-- инициализация -->
	<listener>
		<listener-class>my.app.jaas.Initializer</listener-class>
	</listener>

  • JSF mapping — будем использовать xhtml формат страниц
  • Настройка безопасности: ссылаемся на серверный логин-модуль, настроенный ранее; аутентификация с использованием формы; далее определяем 2 роли и назначаем пути к защищаемым ресурсам
  • Инициализация — определяем класс, код которого должен быть выполнен при старте приложения. Тут мы сможем создать необходимые записи в базе данных (при первом запуске в базе должен быть создан пользователь с ролью администратора)

Настройка Maven: дополнительные зависимости в pom.xml


откроем pom.xml и добавим зависимости:
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>4.0.1.Final</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.richfaces.core</groupId>
			<artifactId>richfaces-core-impl</artifactId>
			<version>4.2.2.Final</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.richfaces.ui</groupId>
			<artifactId>richfaces-components-ui</artifactId>
			<version>4.2.2.Final</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.6</version>
		</dependency>

  • hibernate — необязательная зависимость, просто немного читерства, а в принципе можно обойтись возможностями JPA
  • richfaces — большой набор компонент, расширенная поддержка ajax, несколько готовых скинов, встроенная библиотека jQuery, короче, облегчение жизни при программировании клиентской части. Можно заменить на IceFaces, PrimeFaces или любую другую понравившуюся библиотеку.
  • commons-codec — понадобится для кодирования хешей в base64

Настройка Java Persistence


Добавим в проект JPA. Для этого откроем свойства проекта и найдем раздел Project Facets, в данном разделе надо поставить галочку напротив JPA. Будет автоматически создан файл persistence.xml. Далее можно настроить доступ к базе в этом файле, а можно передать настройку в hibernate.cfg.xml. Я предпочитаю второе, так как в этом случае под рукой оказывается удобный графический интерфейс, а также есть возможность сделать reverse engineering из существующей базы.
Для второго способа необходимо:
— в persistence.xml сослаться на hibernate.cfg.xml:
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
	xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
	<persistence-unit name="test">
		<properties>
			<property name="hibernate.ejb.cfgfile" value="/hibernate.cfg.xml" />
		</properties>
	</persistence-unit>
</persistence>

— в папку src/main/resources добавить файл hibernate.cfg.xml следующего содержания:
hibernate.cfg.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC 
	"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
	"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory name="">
		<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
		<property name="hibernate.format_sql">true</property>
		<property name="hibernate.show_sql">false</property>
		<property name="hibernate.hbm2ddl.auto">update</property>
		<property name="hibernate.connection.datasource">java:jboss/datasources/MysqlDS</property>
	</session-factory>
</hibernate-configuration>

Обратите внимание на hibernate.hbm2ddl.auto: значение update позволяет автоматически обновлять схему данных, чтобы она соответствовала модели, и нам не придется писать ни строчки DDL для этой базы!
format_sql и show_sql могут пригодиться при отладке;
Закладка редактора «Session Factory» предоставляет еще кучу настроек, но в пока они не понадобятся.
На этом настройку можно считать завершенной.

Модель данных


Данные описываются 2 классами. Связь много-ко-многим описывается с обеих сторон множествами. Хозяином связи будет AppUser (AppRole редко изменяется, это скорее справочник, чем сущность).
Поскольку в MySql отсутствуют последовательности и автоинкремент, для генератора выбрана стратегия GenerationType.TABLE. Остальное, думаю, понятно из аннотаций.
AppUser.java
import java.util.*;
import javax.persistence.*;

@ Entity
@ Table(name = "APP_USER", uniqueConstraints = @ UniqueConstraint(columnNames = "USER_NAME"))
public class AppUser implements java.io.Serializable {

	private Long userId;
	private String userName;
	private String displayName;
	private String pwd;
	private Boolean enabled;
	private Set<AppRole> roles = new HashSet<AppRole>(0);

	@ TableGenerator(
			name = "UserIdGen",
			table = "APP_GEN",
			pkColumnName = "GEN_NAME", 
			pkColumnValue = "USER_ID", 
			valueColumnName = "GEN_VAL", 
			allocationSize = 10)

	@ Id
	@ Column(name = "USER_ID", nullable = false)
	@ GeneratedValue(strategy=GenerationType.TABLE, generator="UserIdGen")	
	public Long getUserId() {
		return this.userId;
	}
	public void setUserId(Long userId) {
		this.userId = userId;
	}

	@ Column(name = "USER_NAME", nullable = false, length = 30)
	public String getUserName() {
		return this.userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}

	@ Column(name = "DISPLAY_NAME", length = 250)
	public String getDisplayName() {
		return this.displayName;
	}
	public void setDisplayName(String displayName) {
		this.displayName = displayName;
	}

	@ Column(name = "PWD", length = 30)
	public String getPwd() {
		return this.pwd;
	}
	public void setPwd(String pwd) {
		this.pwd = pwd;
	}

	@ Column(name = "ENABLED")
	public Boolean getEnabled() {
		return this.enabled;
	}
	public void setEnabled(Boolean enabled) {
		this.enabled = enabled;
	}

	@ ManyToMany(fetch = FetchType.LAZY, cascade=CascadeType.ALL)
	@ JoinTable(
			name = "APP_MEMBERSHIP", 
			joinColumns = { 
					@ JoinColumn(name = "USER_ID", nullable = false, updatable = false)
			},
			inverseJoinColumns = { 
					@ JoinColumn(name = "ROLE_ID", nullable = false, updatable = false) 
			})
	public Set<AppRole> getRoles() {
		return this.roles;
	}
	public void setRoles(Set<AppRole> roles) {
		this.roles = roles;
	}
}

Для пароля указана длина 30: этого должно хватить для SHA-1 дайджеста (20 байт) в кодировке base64;
Для поля enabled указан тип Boolean, не всякий сервер это поймет (например, в FirebirdSQL придется создать домен с таким именем), но MySql его интерпретирует без вопросов.
AppRole.java
import java.util.*;
import javax.persistence.*;

@ Entity
@ Table(name = "APP_ROLE", uniqueConstraints = @ UniqueConstraint(columnNames = "ROLE_NAME"))
public class AppRole implements java.io.Serializable {

	private Long roleId;
	private String roleName;
	private String displayName;
	private Set<AppUser> users = new HashSet<AppUser>(0);

	@ TableGenerator(
			name = "RoleIdGen",
			table = "APP_GEN",
			pkColumnName = "GEN_NAME", 
			pkColumnValue = "ROLE_ID", 
			valueColumnName = "GEN_VAL", 
			allocationSize = 10)

	@ Id
	@ Column(name = "ROLE_ID", nullable = false)
	@ GeneratedValue(strategy=GenerationType.TABLE, generator="RoleIdGen")	
	public Long getRoleId() {
		return this.roleId;
	}
	public void setRoleId(Long roleId) {
		this.roleId = roleId;
	}

	@ Column(name = "ROLE_NAME", length = 30)
	public String getRoleName() {
		return this.roleName;
	}
	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}

	@ Column(name = "DISPLAY_NAME", length = 250)
	public String getDisplayName() {
		return this.displayName;
	}
	public void setDisplayName(String displayName) {
		this.displayName = displayName;
	}

	@ ManyToMany(fetch = FetchType.LAZY, mappedBy = "roles")
	public Set<AppUser> getUsers() {
		return this.users;
	}
	public void setUsers(Set<AppUser> users) {
		this.users = users;
	}
}

При желании в проект также можно добавить класс AppGen, который будут соответствовать таблице генераторов APP_GEN, для того, чтобы наше приложение могло работать с устаревшими (legacy) SQL серверами. Дело в том, что по умолчанию в таблице APP_GEN будет создано поле — первичный ключ GEN_NAME длиной 256 символов, что не всегда поддерживается, и эту длину можно уменьшить, явно указав в аннотации. По мне так достаточно и 30 символов (см.например длину названий последовательностей в Oracle).

Инициализация приложения


Инициализацию приложения будет выполнять тот самый класс my.app.jaas.Initializer, который был ранее указан в web.xml
Initializer.java
@ ManagedBean
public class Initializer implements ServletContextListener  {

    private static final Logger log = Logger.getLogger(Initializer.class);

	@Override
	public void contextDestroyed(ServletContextEvent event) {}

	@Override
	public void contextInitialized(ServletContextEvent event) {
		loadData();
	}

	@ PersistenceContext
	EntityManager em;

	private AppRole checkRole(String roleName, String displayName, Session session) {
		AppRole role =
				(AppRole)session.createCriteria(AppRole.class)
				.add(Restrictions.eq("roleName", roleName))
				.uniqueResult();
		if (role == null) {
			role = new AppRole();
			role.setRoleName(roleName);
			role.setDisplayName(displayName);
			session.save(role);
		}
		return role;
	}

	private void loadData() {
		Session session = (Session) em.getDelegate();
		
		AppRole adminRole = checkRole("Admin", "Администраторы", session);
		checkRole("Manager", "Менеджеры", session);

		if (adminRole.getUsers().size()==0) {
			AppUser user = 
					(AppUser)session.createCriteria(AppUser.class)
					.add(Restrictions.eq("userName", "admin"))
					.uniqueResult();
			if(user==null) {
				user = new AppUser();
				user.setUserName("admin");
				user.setDisplayName("Администратор");
				user.setPwd(encode("topsecret"));
				user.setEnabled(true);
				session.save(user);
			}
			adminRole.getUsers().add(user);//nothing
			user.getRoles().add(adminRole);
			session.save(adminRole);
			session.save(user);
		}
		session.flush();
		session.close();
	}

	public static String encode(String value) {
		//get the message digest
		try{
			MessageDigest md = MessageDigest.getInstance("SHA"); //SHA-1 algorithm
			md.update(value.getBytes("UTF-8")); //byte-representation using UTF-8 encoding format
			byte raw[] = md.digest();
			String hash = Base64.encodeBase64String(raw).trim();
			return hash;
		} catch(Exception e) {
			log.error(e, e);
		}

		return value;
	}

	public String logout() {
		FacesContext ctx = FacesContext.getCurrentInstance();
		HttpSession session = (HttpSession)ctx.getExternalContext().getSession(false);
		session.invalidate();
		return("logout");
	}

}

Как видно, реализован единственный метод-слушатель ServletContextListener.contextInitialized, в котором проверяются и при необходимости создаются роли, а также проверяется наличие хотя бы 1 администратора. При отсутствии администратора создается учетная запись admin.
Статический метод encode можно будет использовать в модуле управления пользователями.
Также нам пригодится еще 1 метод logout(), с очевидным назначением.
Работа с базой данных в данном случае ведется не через JPA, а на уровень ниже — через hibernate API, в результате можно использовать замечательный интерфейс org.hibernate.Criteria и выполнить все действия без единой строчки на sql, hql или jpql.

Форма аутентификации


login.xhtml
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:a4j="http://richfaces.org/a4j"
	xmlns:rich="http://richfaces.org/rich"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:c="http://java.sun.com/jsp/jstl/core"
	xmlns:h="http://java.sun.com/jsf/html">
<h:head>
	<title>Вход в систему</title>
	<h:outputStylesheet>
div.login-container {
	width: 255px;
	position: relative;
	margin: 0 auto 0 auto;
}
	</h:outputStylesheet>
</h:head>
<h:body>
	<div class="login-container" id="login_container">
		<rich:panel>
			<f:facet name="header">
				<h:outputText value="Вход в систему" />
			</f:facet>
			<form method="post" action="j_security_check" name="loginform"
				id="loginForm" target="_parent">
				<h:panelGrid columns="2" cellpadding="2"
					columnClasses="right,left" width="100%">
					<h:outputLabel for="j_username" value="Логин:" />
					<h:inputText style="width: 155px;" 
						id="j_username" value="" />
					<h:outputLabel for="j_password" value="Пароль:" />
					<h:inputSecret style="width: 155px;" 
						id="j_password" value="" />

					<h:panelGroup />
					<h:panelGroup />
					<h:panelGroup />
					<h:panelGroup>
						<h:commandButton name="login" id="login-submit" value="Вход" />
						<h:outputText value=" " escape="false"/>
						<h:commandButton type="button" id="login-cancel"
							value="Отмена" />
					</h:panelGroup>
				</h:panelGrid>
			</form>
		</rich:panel>
	</div>
	<h:outputScript>
(function(){
	jQuery("#login_container").offset({top:Math.max(0,(jQuery(window).height()/2)-150)});
	var el = jQuery("#j_username").get(0);el.focus();el.select();
})();
	</h:outputScript>
</h:body>
</html>

Тут можно рисовать любую форму, единственное требование — на сервер должны сабмититься значения j_username и j_password. Поскольку в данном случае используются компоненты richfaces, то в код страницы автоматически включается jQuery, возможности которого и используются в скрипте для позиционирования контейнера login-container и автоматического выделения элемента с именем пользователя.

Итак, все готово для первого запуска. Далее помещаем любой контент в каталоги webapp/view, webapp/admin, коммитим изменения на сервер, и после запуска приложения убеждаемся, что доступ в эти каталоги возможен только после аутентификации и при наличии соответствующих ролей.
При старте приложения в базе данных будут автоматически созданы необходимые таблицы и записи, в этом можно убедиться установив картридж phpmyadmin, либо включив трассировку запросов в файле hibernate.cfg.xml:
		<property name="hibernate.show_sql">false</property>


Выводы


На приведенном выше примере была рассмотрена разработка приложения с аутентификацией для OpenShift. Это же приложение можно скомпилировать и использовать на любом другом сервере JBoss AS 7.1 и с любым из поддерживаемых sql диалектов. Различие будет только в расположении файла настройки standalone.xml, и в необходимости установки нужного jdbc модуля.
При настройке подключения к источнику данных следует помнить о кодировке.
В рассмотренном шаблоне использовался минимум подгружаемых библиотек, что немаловажно для ограниченных ресурсов, предоставляемых OpenShift Express. В основном используются модули, уже включенные в дистрибутив JBoss, как результат — экономия дискового пространства и времени публикации приложения.
Tags:
Hubs:
Total votes 9: ↑7 and ↓2+5
Comments2

Articles