1. 简介
在本教程中,我们将演示如何****验证 我们 的 用户 是否 从新设备/位置登录 。
我们将向他们发送登录通知,让他们知道我们在他们的帐户上检测到不熟悉的活动。
2. 用户位置和设备详细信息
我们需要两件事:用户的位置,以及他们用于登录的设备的信息。
考虑到我们使用 HTTP 与用户交换消息,我们将不得不完全依赖传入的 HTTP 请求及其元数据来检索此信息。
对我们来说幸运的是,HTTP 标头的唯一目的就是携带此类信息。
2.1. 设备位置
在我们可以估计用户的位置之前,我们需要获取他们的原始 IP 地址。
我们可以通过使用来做到这一点:
- X-Forwarded-For ——事实上的标准标头,用于识别通过 HTTP 代理或负载平衡器连接到 Web 服务器的客户端的原始 IP 地址
- ServletRequest.getRemoteAddr() – 一种实用方法,返回客户端的原始 IP 或发送请求的最后一个代理
从 HTTP 请求中提取用户的 IP 地址并不十分可靠,因为它们可能会被篡改。但是,让我们在教程中对此进行简化,并假设情况并非如此。
检索到 IP 地址后,我们可以通过 geolocation将其转换为真实世界的位置。
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发送请求。
我们将求助于已经过测试且更可靠的现有解决方案,而不是从头开始实施用户代理解析器。
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](https://search.maven.org/search?q=g:com.github.ua-parser AND a: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。验证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上找到本教程的完整实现。