這篇文章是系列的一部分
• Spring Security 註冊系列
• 使用 Spring Security 的註冊過程
• 註冊 – 通過電子郵件激活新帳户
• Spring Security 註冊 – 發送驗證電子郵件
• 使用 Spring Security 註冊 – 密碼編碼
• Spring Security 註冊 API 變為 RESTful
• Spring Security – 重置您的密碼
• 註冊 – 密碼強度和規則
• 更新您的密碼
• 通知用户從新設備或位置登錄 (當前文章)
1. 簡介
在本教程中,我們將演示如何 驗證 用户 是否 從 新 設備/位置 登錄。
我們將向他們發送登錄通知,讓他們知道我們檢測到他們在他們的帳户上存在未知的活動。
2. 用户位置和設備詳情
我們需要獲取兩類信息:用户的位置以及他們使用登錄時使用的設備信息。
考慮到我們使用 HTTP 交換消息,我們只能依賴傳入的 HTTP 請求及其元數據來獲取這些信息。
幸運的是,有 HTTP 頭部專門用於攜帶此類信息。
2.1. 設備位置
在我們可以估算用户位置之前,我們需要獲取他們的原始 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);
}
有了我們的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 地址後,我們可以藉助 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. 結論
在本文中,我們演示瞭如何在檢測到用户賬户中未知的活動時發送登錄通知。
0 位用戶收藏了這個故事!