新設備或位置登錄通知

Spring Security
Remote
0
04:26 AM · Nov 30 ,2025

1. 簡介

在本教程中,我們將演示如何 驗證 用户 是否 設備/位置 登錄

我們將向他們發送登錄通知,讓他們知道我們檢測到他們在他們的帳户上存在未知的活動。

2. 用户位置和設備詳情

我們需要獲取兩類信息:用户的位置以及他們使用登錄時使用的設備信息。

考慮到我們使用 HTTP 交換消息,我們只能依賴傳入的 HTTP 請求及其元數據來獲取這些信息。

幸運的是,有 HTTP 頭部專門用於攜帶此類信息。

2.1. 設備位置

在我們可以估算用户位置之前,我們需要獲取他們的原始 IP 地址。

我們可以通過使用以下方法:

  • X-Forwarded-For – 識別通過 HTTP 代理或負載均衡連接到 Web 服務器的客户端的原始 IP 地址的行業標準頭部
  • ServletRequest.getRemoteAddr() – 返回客户端或發送請求的最後一個代理的原始 IP

從 HTTP 請求中提取用户的 IP 地址並不總是可靠的,因為它們可能會被篡改。但是,為了簡化我們的教程,我們假設這種情況不會發生。

一旦我們獲取了 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;
    //...
}

以及它的Repository

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

有了我們的EntityRepository就位,我們就可以開始收集我們需要記錄用户設備及其位置的信息。

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中的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. 結論

在本文中,我們演示瞭如何在檢測到用户賬户中未知的活動時發送登錄通知。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.