Перейти к основному содержимому

Уведомить пользователя о входе в систему с нового устройства или местоположения

· 5 мин. чтения

1. Введение

В этом руководстве мы собираемся продемонстрировать, как мы можем проверить , входят ли наши пользователи в систему с нового устройства /местоположения . ** ** ** ** ** ** ****

Мы собираемся отправить им уведомление о входе в систему, чтобы сообщить им, что мы обнаружили незнакомую активность в их учетной записи.

2. Местоположение пользователей и сведения об устройстве

Нам нужны две вещи: местонахождение наших пользователей и информация об устройствах, которые они используют для входа в систему.

Учитывая, что мы используем HTTP для обмена сообщениями с нашими пользователями, нам придется полагаться исключительно на входящий HTTP-запрос и его метаданные для получения этой информации.

К счастью для нас, существуют заголовки HTTP, единственной целью которых является передача такого рода информации.

2.1. Местоположение устройства

Прежде чем мы сможем оценить местоположение наших пользователей, нам нужно получить их исходный IP-адрес.

Мы можем сделать это, используя:

  • X-Forwarded-For — стандартный заголовок де-факто для определения исходного IP-адреса клиента, подключающегося к веб-серверу через HTTP-прокси или балансировщик нагрузки.
  • ServletRequest.getRemoteAddr() — служебный метод, который возвращает исходный IP-адрес клиента или последнего прокси-сервера, отправившего запрос.

Извлечение IP-адреса пользователя из HTTP-запроса не совсем надежно, поскольку они могут быть подделаны. Однако давайте упростим это в нашем руководстве и предположим, что это не так.

Получив IP-адрес, мы можем преобразовать его в реальное местоположение с помощью геолокации .

2.2. Сведения об устройстве

Подобно исходному IP-адресу, существует также HTTP-заголовок, который содержит информацию об устройстве, которое использовалось для отправки запроса, называемого User-Agent .

Короче говоря , он несет информацию , которая позволяет нам идентифицировать тип приложения , операционную систему и поставщика / версию программного обеспечения запрашивающего пользовательского агента . `` ** ** ** ** ****

Вот пример того, как это может выглядеть:

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 
(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36

В нашем примере выше устройство работает под управлением Mac OS X 10.14 и использует Chrome 71.0 для отправки запроса.

Вместо того, чтобы внедрять синтаксический анализатор User-Agent с нуля, мы собираемся прибегнуть к существующим решениям, которые уже протестированы и являются более надежными.

3. Обнаружение нового устройства или местоположения

Теперь, когда мы представили необходимую информацию, давайте изменим наш AuthenticationSuccessHandler , чтобы он выполнял проверку после входа пользователя в систему:

public class MySimpleUrlAuthenticationSuccessHandler 
implements AuthenticationSuccessHandler {
//...
@Override
public void onAuthenticationSuccess(
final HttpServletRequest request,
final HttpServletResponse response,
final Authentication authentication)
throws IOException {
handle(request, response, authentication);
//...
loginNotification(authentication, request);
}

private void loginNotification(Authentication authentication,
HttpServletRequest request) {
try {
if (authentication.getPrincipal() instanceof User) {
deviceService.verifyDevice(((User)authentication.getPrincipal()), request);
}
} catch(Exception e) {
logger.error("An error occurred verifying device or location");
throw new RuntimeException(e);
}
}
//...
}

Мы просто добавили вызов к нашему новому компоненту: DeviceService . Этот компонент будет инкапсулировать все, что нам нужно для идентификации новых устройств/местоположений и уведомления наших пользователей.

Однако, прежде чем мы перейдем к нашему DeviceService , давайте создадим нашу сущность DeviceMetadata для сохранения данных наших пользователей с течением времени:

@Entity
public class DeviceMetadata {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long userId;
private String deviceDetails;
private String location;
private Date lastLoggedIn;
//...
}

И его репозиторий :

public interface DeviceMetadataRepository extends JpaRepository<DeviceMetadata, Long> {
List<DeviceMetadata> findByUserId(Long userId);
}

Имея сущность и репозиторий , мы можем начать сбор информации, необходимой для ведения учета устройств наших пользователей и их местоположения.

4. Извлечение местоположения нашего пользователя

Прежде чем мы сможем оценить географическое положение нашего пользователя, нам нужно извлечь его IP-адрес:

private String extractIp(HttpServletRequest request) {
String clientIp;
String clientXForwardedForIp = request
.getHeader("x-forwarded-for");
if (nonNull(clientXForwardedForIp)) {
clientIp = parseXForwardedHeader(clientXForwardedForIp);
} else {
clientIp = request.getRemoteAddr();
}
return clientIp;
}

Если в запросе есть заголовок X-Forwarded-For , мы будем использовать его для извлечения их IP-адреса; в противном случае мы будем использовать метод getRemoteAddr() .

Получив их IP-адрес, мы можем оценить их местоположение с помощью Maxmind :

private String getIpLocation(String ip) {
String location = UNKNOWN;
InetAddress ipAddress = InetAddress.getByName(ip);
CityResponse cityResponse = databaseReader
.city(ipAddress);

if (Objects.nonNull(cityResponse) &&
Objects.nonNull(cityResponse.getCity()) &&
!Strings.isNullOrEmpty(cityResponse.getCity().getName())) {
location = cityResponse.getCity().getName();
}
return location;
}

5. Данные об устройствах пользователей ****

Поскольку заголовок User-Agent содержит всю необходимую нам информацию, остается только извлечь ее. Как мы упоминали ранее, с помощью парсера User-Agent (в данном случае uap-java ) получить эту информацию становится довольно просто:

private String getDeviceDetails(String userAgent) {
String deviceDetails = UNKNOWN;

Client client = parser.parse(userAgent);
if (Objects.nonNull(client)) {
deviceDetails = client.userAgent.family
+ " " + client.userAgent.major + "."
+ client.userAgent.minor + " - "
+ client.os.family + " " + client.os.major
+ "." + client.os.minor;
}
return deviceDetails;
}

6. Отправка уведомления о входе

Чтобы отправить уведомление о входе в систему нашему пользователю, нам нужно сравнить информацию, которую мы извлекли, с прошлыми данными, чтобы проверить, видели ли мы уже устройство в этом месте в прошлом.

Давайте взглянем на наш DeviceService. проверить метод Device() :

public void verifyDevice(User user, HttpServletRequest request) {

String ip = extractIp(request);
String location = getIpLocation(ip);

String deviceDetails = getDeviceDetails(request.getHeader("user-agent"));

DeviceMetadata existingDevice
= findExistingDevice(user.getId(), deviceDetails, location);

if (Objects.isNull(existingDevice)) {
unknownDeviceNotification(deviceDetails, location,
ip, user.getEmail(), request.getLocale());

DeviceMetadata deviceMetadata = new DeviceMetadata();
deviceMetadata.setUserId(user.getId());
deviceMetadata.setLocation(location);
deviceMetadata.setDeviceDetails(deviceDetails);
deviceMetadata.setLastLoggedIn(new Date());
deviceMetadataRepository.save(deviceMetadata);
} else {
existingDevice.setLastLoggedIn(new Date());
deviceMetadataRepository.save(existingDevice);
}
}

После извлечения информации мы сравниваем ее с существующими записями DeviceMetadata , чтобы проверить, есть ли запись, содержащая ту же информацию:

private DeviceMetadata findExistingDevice(
Long userId, String deviceDetails, String location) {
List<DeviceMetadata> knownDevices
= deviceMetadataRepository.findByUserId(userId);

for (DeviceMetadata existingDevice : knownDevices) {
if (existingDevice.getDeviceDetails().equals(deviceDetails)
&& existingDevice.getLocation().equals(location)) {
return existingDevice;
}
}
return null;
}

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

В противном случае мы просто обновляем атрибут lastLoggedIn знакомого устройства.

7. Заключение

В этой статье мы продемонстрировали, как мы можем отправить уведомление о входе в систему, если обнаружим незнакомую активность в учетных записях пользователей.

Полную реализацию этого руководства можно найти на Github .