пятница, 31 октября 2008 г.

Event-Driven Development в Spring. Часть 1

Еще в Spring 1.2 была возможность посылать различные event-ы через весь контекст приложения. Я считаю этот вид коммуникации в некоторых местах приложения единственным верным решением.

Опишу кратко как работает этот механизм. Базовым классом для event-а должен быть org.springframework.context.ApplicationEvent, для рассылки которого используется метод org.springframework.context.ApplicationContext.publishEvent(event). Ну а получать event-ы будут те bean-ы, которые реализуют интерфейс org.springframework.context.ApplicationListener с единственным методом onApplicationEvent(event).

Допустим, что у нас есть логика, после выполнения которой другим сервисам необоходимо также сделать какие-то действия. Стандартный вариант решения такой задачи предполагает использование IoC:

public class BussinesServiceImpl implements BussinesService {

@Autowired
private EmailService emailService;
@Autowired
private LoggingService logService;

public void doAction() {

...

emailService.sendEmail(email, body);
logService.recordLog(data);
}
}

Также можно использовать и аспекты. Только вот получать параметры, которые пришли не через аргументы метода, advice-ам будет сложно. Реализовать такой случай с помощью event-ов можно так:

public class BussinesServiceImpl implements BussinesService, ApplicationContextAware {

private ApplicationContext context;

public void doAction() {

...

ActionEvent event = new ActionEvent();
event.setEmail(email);
event.setEmailBody(body);
event.setData(data);
context.publishEvent(event);
}

public void setApplicationContext(ApplicationContext value) {
context = value;
}
}

Соответственно сами сервисы будут выглядеть так:

public class EmailServiceImpl implements EmailService, ApplicationListener {

...

public void onApplicationEvent(ApplicationEvent event) {
if (!(event instanceof ActionEvent)) {
return;
}

ActionEvent actionEvent = (ActionEvent) event;
sendEmail(actionEvent.getEmail(), actionEvent.getEmailBody());
}
}

public class LoggingServiceImpl implements LoggingService, ApplicationListener {

...

public void onApplicationEvent(ApplicationEvent event) {
if (!(event instanceof ActionEvent)) {
return;
}

ActionEvent actionEvent = (ActionEvent) event;
recordLog(actionEvent.getData());
}
}

Какое преимущество дает такой подход? Изоляцию сервиса, от лишних зависимостей. Такая изоляция дает возможность выносить сервисы типа BussinesService в отдельные модули, которые не потребуют в дальнейшем модификации, если понадобится добавить вызов какого-либо сервиса.

среда, 29 октября 2008 г.

Идентификация пользователя в браузере. Как узнать IP и MAC в Java-апплете.

Задача: необходимо чтобы пользователь, с одного компьютера, имел лишь одну сессию с сервером.

При этом нужно учитывать, что пользователь может использовать разные браузеры, так что зацепка за какой-нибудь параметр http-заголовока типа User-agent ничего не даст. Однако возможно получить реальный ip-адрес клиента в подсети, а не возможного прокси, который мы получаем через HttpServletRequest.getLocalAddr(). Редко сейчас у клиентов есть выделенные ip. Также, помимо ip можно получить и mac-адрес! Все что для этого нужно так это написать Java-Applet, который запустится у клиента в браузере и передаст нужную нам информацию.

Вообще возможность определение ip адреса во внутренней сети пришла в голову некому Lars Kindermann-у в 2002-ом году. Судя по фотке он проживает где-то на полярном круге .-) Его идея оказалась принципиально простой. Java-Applet создает подключение к серверу, с которого его скачали, через объект java.net.Socket. В результате мы легко можем получить сетевую информацию о клиенте.

Ну а чтобы получить mac-адрес, не придется даже создавать соединения, можно просто сделать вызов java.net.NetworkInterface.getHardwareAddress(). Можно было бы довольствоваться лишь mac адресом. Однако, возможность его получения появилась лишь в JRE 1.6.

В решении я использовал, как обычно, Spring Security 2.0.4. Идея следующая, после аутентификации пользователя мы проверяем есть ли такой же клиент, с тем же ip и mac-ом, уже в системе. Для этого необходимо, чтобы фильтр аутентификации перебросил нас на страничку с апплетом. Затем уже апплет собирает сетевую информацию о клиенте и отсылает ее на сервер простым GET-ом, в ответ на этот запрос, сервер, выполнив проверку, посылает редирект на следующую страничку, либо выдает ошибку. При получении запроса сервером, составляется композитный id клиента, в нем учавствует как mac и локальный ip клиента, пришедшие от апплета, так и ip шлюза с которым соединен сервер и за которым находится сам клиент. Я свел воедино оба решения, использовать нужно либо ip+ip шлюза, либо mac вместе их использовать уже избыточно. Если есть возможность, и в требованиях вашего приложения предусмотрена возможность установки JRE 1.6 на машину клиента, то лучше использовать mac. Способ идентификации основанный только на ip имеет недостаток. В случае если за одним шлюзом может находится несколько подсетей, а следовательно и клиентов с одинаковым адресом может быть тоже несколько.

Вот собственно и сам исходный код апплета:

import java.applet.Applet;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.Enumeration;

public class UserApplet extends Applet {

/**
* Запуск апплета
*/
public void start() {
String remoteHost = getDocumentBase().getHost();
int remotePort = getDocumentBase().getPort();
String ip = getLocalIp(remoteHost, remotePort);
String mac = getLocalMac();
sendInfo(ip, mac);
}

/**
* Получение локального mac
*/
private String getLocalMac() {
try {
Enumeration nis = NetworkInterface.getNetworkInterfaces();
while(nis.hasMoreElements()) {
String mac = macToString((NetworkInterface) nis.nextElement());
if (mac != null) {
return mac;
}
}
} catch (SocketException e) {
showStatus("");
}
return null;
}

private static String macToString(NetworkInterface ni)
throws SocketException {
byte mac[] = ni.getHardwareAddress();
if(mac != null) {
StringBuffer macAddress = new StringBuffer();
String sep = "";
for (int i = 0; i < mac.length; i++) {
int b = Math.abs(mac[i]);
String hexByte = Integer.toHexString(b);
macAddress.append(sep).append(hexByte);
sep = ":";
}
return macAddress.toString();
}
return null;
}

/**
* Получение локального ip
*/
private String getLocalIp(String remoteHost, int remotePort) {
Socket socket = null;
try {
socket = new Socket(remoteHost, remotePort);
String localIp = socket.getLocalAddress().getHostAddress();
return localIp;
} catch (Exception e) {
showStatus("");
} finally {
if (socket != null) {
socket.close();
}
}
return "unknown";
}

/**
* Отправляем информацию об ip и mac
*/
private void sendInfo(String ip, String mac) {
try {
URL url = new URL(getDocumentBase(), "/userInfo?ip=" + ip + "&mac=" + mac);
getAppletContext().showDocument(url, "_self");
} catch (Exception e) {
showStatus("");
}
}
};

Апплет загружает ответом нашего Spring Security фильтра ru.someone.UserInfoFilter содержимым:

<applet code="UserApplet.class" codebase="/" codetype="application/java" width="0" height="0"></applet>

После чего этот же фильтр делает необходимую проверку и посылает ответ. При логауте из системы, данные о клиенте удаляются вместе с его сессией. Чтобы это действительно было так, в web.xml нужно прописать listener: org.springframework.security.ui. session.HttpSessionEventPublisher. Который будет посылает эвенты об удалении сессии по всему Spring-контексту, а наш ru.someone.UserInfoFilter будет их получать.

Исходники можно взять здесь

вторник, 28 октября 2008 г.

Развивать вместе

Как вы уже должно быть заметили, рядом с элементом прокрутки окна появился рыжая кнопка "Оставьте свой отзыв". Я разместил ее с одной целью - чтобы вы писали о том, чего не хватает в моем блоге, свои идеи и предложения.

Миграция данных с LiquiBase

Во всех проектах, в которых мне довелось принимать участие, реализуемых на Java-платформе, я наблюдал примерно одну и ту же картину. Когда дело доходило до релиза и дальнейшего сопровождения изменений в БД, разработчикам всегда приходилось писать, либо уже использовать некий самописный механизм инкрементальной миграции данных.

Однако, я был убежден в том, что такого рода фреймворк должен быть уже кем-то написан. Им оказался LiquiBase написанный на той же Java и выпущенный под лицензией LGPL. Вот основные фичи этого проекта:

  • Не зависит от реализации БД, поддерживает: MySQL, PostgreSQL, Oracle, MS SQL, DB2, Derby, HSQL
  • Возможность проведения миграций от нескольких пользователей одновременно
  • Поддержка rollback-а миграций
  • Описание миграций в XML файле, с помощью внутренних команд.
  • Интеграция c Maven, Ant, Spring, Hibernate, Grails, возможность запуска из коммандой строки.

Формат документа, в котором описываются сами миграции выглядит примерно следующим образом:

<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.8"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.8
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.8.xsd">

<changeSet id="1" author="bob">
<comment>Create table<comment>
<createTable tableName="department">
<column name="id" type="int">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="active" type="boolean" defaultValue="1"/>
</createTable>
</changeSet>

<changeSet id="2" author="sam">
<comment>Change name</comment>
<sql>
UPDATE TABLE SET name = 'someone' WHERE name = 'foo';
</sql>
<rollback>
<sql>
UPDATE TABLE SET name = 'foo' WHERE name = 'someone';
</sql>
</rollback>
</changeSet>
</databaseChangeLog>
Также хорошую статью на эту тему можно прочитать здесь.



Использование liquibase с hibernate и maven. Часть 1

понедельник, 27 октября 2008 г.

Отключение кэширования html-страницы. Часть 1

Старая проблема - как отключить кэширование страницы в браузере, но лишний раз напомнить о ней не помешает. Итак, в html странице, нужно использовать следующие meta-тэги:

<html>
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, max-age=0, must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="Fri, 01 Jan 1990 00:00:00 GMT"/>
</head>
<body>
...
</body>
<html>

Значение параметера http-equiv должно быть названием параметра используемого непосредственно в http-заголовке. В content указываем само значение параметра. Однако это вовсе не означает, что вы увидите их в самом http-заголовке, хотя есть серваки, которые автоматически встраивают такого рода meta-тэги в http-заголовок.

Кратко об используемых параметрах:

  • Cache-Control - параметер управления кешированием страниц. Введен в HTTP 1.1.
  • Pragma - отключение кэширования страницы. Имеет единственное значение no-cache. Введен в HTTP 1.0.
  • Expires - устанавливает дату и время, после которого документ считается устаревшим. Дата должна указываться в следующем формате (на английском языке): День недели (сокр.) число (2 цифры) Месяц (сокр.) год часы:минуты:секунды GMT. Введен в HTTP 0.9

Т.к. не известно заранее какая версия HTTP-протокола будет использоваться на стороне клиента, то лучше использовать все три параметра.

воскресенье, 26 октября 2008 г.

ConceptDraw Office - хорошая замена MS Visio и MS Project

Хочу представить вашему вниманию такой замечательный пакет, как ConceptDraw Office. Используя этот пакет, можно управлять всеми аспектами проекта - от мозгового штурма до создания команды, планирования, распределения ресурсов и анализа результатов. В состав пакета входят продукты ConceptDraw MINDMAP, ConceptDraw PROJECT и ConceptDraw PRO - в таком единстве, набор обеспечивает все возможности для управлениями рабочими проектами: планирование и составление графика, организация процесса работы, процесс отслеживания и управления проектом.



Интерфейс выполнен в стиле MacOS - элементы на диаграммах с закругленными краями, гибкие связи и т.д. Соответственно и сам пакет существует под две платформы MacOS и Windows.

Я посмотрел лишь ConceptDraw PRO и ConceptDraw PROJECT. ConceptDraw PRO - полноценная замена Microsoft Visio, имеет богатый набор графических компонентов, с первого взгляда впечатлила фича авто-лайаута по краям соседних элементов. ConceptDraw PROJECT - отличная замена Microsoft Project, с возможность экспорта и импорта из него. ConceptDraw PROJECT более корректно работает с единицей измерения длительности задач - час. К примеру, не пишет для группы задач выраженных в часах, длительность в 1,78 дней. А воозможность экспорта набора задач в ConceptDraw MINDMAP оставила неизгладимое впечатление.

Вообщем, рекомендую к использованию!

пятница, 24 октября 2008 г.

Logout в Spring Security используя BlazeDS

В продолжение темы программной аутентификации, решил описать программный способ logout-а в BlazeDS. Я знаю, что кому-то это может очень пригодиться .-) Необоходимо сделать logout на уровне бизнес-логики, которая о javax.servlet.http.HttpSession ничего не знает и знать не может. Покопавшись в исходниках flex.messaging.FlexSession
и его подклассе flex.messaging.HttpFlexSession я нашел замечательный метод invalidate. Опять же, код в три строчки:

public void logout() {
HttpFlexSession flexSession = (HttpFlexSession) FlexContext.getFlexSession();
flexSession.invalidate(false);
SecurityContextHolder.clearContext();
}

Программная аутентификация в Spring Security 2.0

Порой возникает необходимость произвести аутентификацию в приложении программно, а не через POST-запрос. В Spring Security это можно сделать с помощью, буквально, трех строк кода:

@Autowired
private AuthenticationManager _authenticationManager;

private void authenticate(HttpServletRequest request, String nick, String password) {
Authentication auth = new UsernamePasswordAuthenticationToken(nick, password);
WebAuthenticationDetails details = new WebAuthenticationDetails(request);
auth.setDetails(details);
Authentication fullauth = _authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(fullauth);
}

среда, 22 октября 2008 г.

Проблема логина через https в Spring Security 2.0

Довольно тривиальная задача, как мне казалось, сделать логин на https-соединении с дальнейшим редиректом на страницу доступную по обычному http-каналу. Для реализации такого решения использовался Spring Security 2.0.4. Однако, все оказалось не так просто.
Для начала приведу свою настройку security-контекста:

<sec:http auto-config="true">
<sec:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https"/>
<sec:intercept-url pattern="/index.jsp" access="ROLE_PLAYER" requires-channel="http"/>
<sec:form-login login-page="/login.jsp" default-target-url="/index.jsp" />
</sec:http>

Конфигурация правильная, только вот редирект не работает. Проблема в том, что после успешной аутентификации, меня перебрасывает обратно на login.jsp.

Воспользовашись HttpWatch-ем, я выяснил, что проблема в том, что при переходе, допустим, с линка https://localhost:8443/login.jsp на линк http://localhost:8080/index.jsp, jsessionid передавалась в куке через http-заголовок, а не через урл, как полагается в этом случае. Поэтому Spring Security не видел ранее созданной сесси и создавал свою. Кстати, по умолчанию, после успешной аутентификации он создает новую сессию, дабы предотвратить атаку Session Fixation.

Я завел на это багу http://jira.springframework.org/browse/SEC-1019. Не знаю, когда она будет закрыта, думаю, что не скоро. Поэтому могу предложить свое, временное, решение этой проблемы.
В конфигурации security-контекста пишем:

<sec:http entry-point-ref="authEntryPoint">
<sec:anonymous/>
<sec:port-mappings>
<sec:port-mapping http="8080" https="8443"/>
</sec:port-mappings>
<sec:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https"/>
<sec:intercept-url pattern="/index.jsp" access="ROLE_PLAYER" requires-channel="http"/>
</sec:http>

<bean id="authEntryPoint" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="authFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
<sec:custom-filter position="AUTHENTICATION_PROCESSING_FILTER" />
<property name="filterProcessesUrl" value="/j_spring_security_check"/>
<property name="defaultTargetUrl" value="/"/>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="targetUrlResolver">
<bean class="ru.someone.security.ExTargetUrlResolverImpl">
<property name="url" value="/index.jsp"/>
</bean>
</property>
</bean>

<sec:authentication-manager alias="authenticationManager"/>

Столько xml-я пришлось написать лишь для того, чтобы использовать свою реализацию org.springframework.security.ui.TargetUrlResolver, по-другому никак. Вот собственно и сама реализация этого интерфейса:

package ru.someone.security;

import javax.servlet.http.HttpServletRequest;
import org.springframework.security.Authentication;
import org.springframework.security.ui.TargetUrlResolverImpl;
import org.springframework.security.ui.savedrequest.SavedRequest;

public class ExTargetUrlResolverImpl extends TargetUrlResolverImpl {

private String url;

@Override
public String determineTargetUrl(SavedRequest savedRequest, HttpServletRequest request,
Authentication auth) {
String sessionId = request.getSession().getId();
return url + ";jsessionid=" + sessionId;
}

public void setUrl(String url) {
this.url = url;
}
}

Вводная часть...

Друзья! И так, свершилось - я завел себе блог! Очень этому рад и надеюсь, что он будет постоянно пополнятся моими идеями .-)