Показаны сообщения с ярлыком spring security. Показать все сообщения
Показаны сообщения с ярлыком spring security. Показать все сообщения

понедельник, 2 февраля 2009 г.

Проблема с SecurityContext-ом при invalidate http-сессии

В Spring Security есть возможность определения допустимого числа параллельных http-сессий от одного пользователя. Как только кол-во свободных сессий исчерпает себя, то по-умолчанию самая первая из них будет уничтожена через метод invalidate() и ее место займет новая. За настройку такой возможности отвечает тэг concurrent-session-control и его параметр max-sessions.

Проблема, которая была обнаружена мной буквально на днях, следующая - при уничтожении сессии, вместо объекта org.springframework.security.context.SecurityContext приходит null. Выявить эту ошибку может следующий код:

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

SecurityContext context = SecurityContextHolder.getContext();
Object principal = context.getAuthentication().getPrincipal();

...

}

Если посмотреть порядок следования фильтров в Spring Security:

  1. CHANNEL_FILTER

  2. CONCURRENT_SESSION_FILTER

  3. SESSION_CONTEXT_INTEGRATION_FILTER

  4. LOGOUT_FILTER

  5. ...

то мы увидим, что фильтр SESSION_CONTEXT_INTEGRATION_FILTER, который как раз и выставляет нужный нам объект SecurityContext, находится после CONCURRENT_SESSION_FILTER фильтра, управляющего параллельными сессиями. Такая "неверная", в данном случае, конфигурация фильтров и приводит к потере SecurityContext-а. Потому завел на это дело багу - SEC-1092. Решения я увидел два - поменять фильтры местами, с помощью конфигурации это делается довольно просто, и выставить объект SecurityContext самому. Второй вариант мне показался более верным. Менять фильтры местами, думаю не стоит, возможно есть причина по которой они расположены именно так.

Нужный нам экземпляр SecurityContext-а можно взять из текущий http-сессии, к которой он и привязан. Хранится он в сессии по имени HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY. Объект класса HttpSessionContextIntegrationFilter как раз и находится на позиции SESSION_CONTEXT_INTEGRATION_FILTER, и выставляет текущий SecurityContext в SecurityContextHolder. Сам код решения взят из этого фильтра и выглядит так:

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

HttpSession session = event.getSession();
SecurityContext context = (SecurityContext) session.getAttribute(HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY);
SecurityContextHolder.setContext(context);


SecurityContext context = SecurityContextHolder.getContext();
Object principal = context.getAuthentication().getPrincipal();

...

}

среда, 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 будет их получать.

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

пятница, 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;
}
}