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

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

2 комментария:

splix комментирует...

Это требование, я так понимаю, от "отдела по борьбе с клиентами"?

Если у клиента возникла необходимость два раза зайти, то дайте ему это сделать, он ведь ваш клиент и он прав, не так ли? Все равно ведь зайдет, настроит privoxy, или вообще запустит вмварь и зайдет, только еще больше будет недоволен доставленными неудобствами.

Никита Кокшаров комментирует...

Процент людей, которые будут поднимать vmware ради этого дела будет гораздо меньше половины и это уже хорошо.