新しいデバイスまたは場所からのログインをユーザーに通知

1. 前書き

このチュートリアルでは、*検証* * if * *私たち* *ユーザー* * are * *ロギング* * in * * from * * a * *新しい* *デバイス/場所*を検証する方法を示します。
アカウントでなじみのないアクティビティを検出したことを知らせるために、ログイン通知を送信します。

2. ユーザーの場所とデバイスの詳細

必要なものは2つあります。ユーザーの場所と、ユーザーがログインに使用するデバイスに関する情報です。
HTTPを使用してユーザーとメッセージを交換していることを考えると、この情報を取得するには、着信HTTPリクエストとそのメタデータのみに依存する必要があります。
幸いなことに、この種の情報を伝えることが唯一の目的であるHTTPヘッダーがあります。

* 2.1。 デバイスの場所*

ユーザーの位置を推定する前に、発信元のIPアドレスを取得する必要があります。
それを行うには、次を使用します。
  • X-Forwarded-For
    HTTPプロキシまたはロードバランサを介してWebサーバーに接続するクライアントの発信元IPアドレスを識別するためのデファクト標準ヘッダー

  • ServletRequest.getRemoteAddr()
    クライアントまたは要求を送信した最後のプロキシの発信元IPを返すユーティリティメソッド

    HTTPリクエストからユーザーのIPアドレスを抽出することは、改ざんされる可能性があるため、あまり信頼できません。 ただし、チュートリアルではこれを単純化し、そうではないと仮定します。
    IPアドレスを取得したら、https://en.wikipedia.org/wiki/Geolocation[_geolocation_]を介して実際の場所に変換できます。

* 2.2。 デバイスの詳細*

発信元IPアドレスと同様に、https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-と呼ばれるリクエストの送信に使用されたデバイスに関する情報を保持するHTTPヘッダーもありますエージェント[_User-Agent_]。
つまり、* the * it * _application_ * * _type_ *、* _operating_ * * _system _、*および* _software_ * * _vendor / version_ * * of * * the * * requesting * * user * *エージェント*。
以下に例を示します。
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. 新しいデバイスまたは場所の検出

必要な情報を導入したので、link:/spring_redirect_after_login[_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;
    //...
}
そしてその_Repository_:
public interface DeviceMetadataRepository extends JpaRepository<DeviceMetadata, Long> {
    List<DeviceMetadata> findByUserId(Long userId);
}
_Entity_と_Repository_を配置すると、ユーザーのデバイスとその場所を記録するために必要な情報の収集を開始できます。

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アドレスを取得したら、link:/geolocation-by-ip-with-maxmind[_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_User-Agent_パーサー(https://search.maven.org/search?q=g:com.github.ua-parser%20AND%20a:uap-java[_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.verifyDevice()_メソッドを見てみましょう:
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. 結論

この記事では、ユーザーのアカウントでなじみのないアクティビティを検出した場合にログイン通知を送信する方法を示しました。
このチュートリアルの完全な実装は、https://github.com/Baeldung/spring-security-registration/ [Github上]にあります。