知識庫 / Spring / Spring Security RSS 訂閱

新設備或位置登錄通知

Spring Security
HongKong
6
01:34 PM · Dec 06 ,2025
This article is part of a series:
• Spring Security 註冊系列
• 使用 Spring Security 的註冊流程
• 通過電子郵件激活新賬户
• Spring Security 註冊 – 發送驗證郵件
• 使用 Spring Security 註冊 – 密碼編碼
• 註冊 API 變為 RESTful
• Spring Security – 重置密碼
• 註冊 – 密碼強度和規則
• 更新密碼
• 通知用户從新設備或位置登錄 (當前文章)

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;
    //...
}

它的 存儲庫

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;
}

如果請求中存在<em>X-Forwarded-For</em> 頭部,我們將使用它來提取他們的 IP 地址;否則,我們將使用<em>getRemoteAddr()</em> 方法。

獲取他們的 IP 地址後,我們可以藉助<em>Maxmind</em> 來估算他們的位置:

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. 用户設備詳情

由於<em User-Agent</em>頭信息包含了我們所需的所有信息,所以這僅僅是提取的過程。正如我們之前提到的,藉助<em User-Agent</em>解析器(這裏使用<a href="https://mvnrepository.com/artifact/com.github.ua-parser/uap-java"><em uap-java</em></a>),這使得獲取這些信息變得非常簡單。

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.