• 使用 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);
}有了我們的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;
}如果請求中存在<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. 結論
在本文中,我們演示瞭如何在檢測到用户賬户中未知的活動時發送登錄通知。