1. 序章

このチュートリアルでは、検証 if our users are logging in from a new device /location

アカウントで見慣れないアクティビティが検出されたことを知らせるために、ログイン通知を送信します。

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

必要なものは2つあります。ユーザーの場所と、ユーザーがログインに使用するデバイスに関する情報です。

HTTPを使用してユーザーとメッセージを交換していることを考えると、この情報を取得するには、着信HTTPリクエストとそのメタデータのみに依存する必要があります。

幸いなことに、この種の情報を伝達することを唯一の目的とするHTTPヘッダーがあります。

2.1. デバイスの場所

ユーザーの位置を推定する前に、ユーザーの発信元IPアドレスを取得する必要があります。

これは、次を使用して行うことができます。

  • X-Forwarded-For –HTTPプロキシまたはロードバランサーを介してWebサーバーに接続しているクライアントの発信元IPアドレスを識別するための事実上の標準ヘッダー
  • ServletRequest.getRemoteAddr() –クライアントの発信元IPまたはリクエストを送信した最後のプロキシを返すユーティリティメソッド

HTTPリクエストからユーザーのIPアドレスを抽出することは、改ざんされている可能性があるため、信頼性が低くなります。 ただし、チュートリアルでこれを単純化して、そうではないと仮定しましょう。

IPアドレスを取得したら、geolocationを使用して実際の場所に変換できます。

2.2. デバイスの詳細

発信元のIPアドレスと同様に、User-Agentと呼ばれるリクエストの送信に使用されたデバイスに関する情報を伝達するHTTPヘッダーもあります。

つまり、識別アプリケーションタイプ動作システムを可能にする情報を伝達します、およびソフトウェアベンダー/バージョン要求ユーザー[ X263X]エージェント。

これがどのように見えるかの例です:

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。verify 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にあります。