Notify User of Login From New Device or Location – 通知用户从新的设备或地点登录

最后修改: 2019年 1月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this tutorial, we’re going to demonstrate how we can verify if our users are logging in from a new device/location.

在本教程中,我们将演示如何验证是否我们的用户正在登录设备/位置登录。

We’re going to send them a login notification to let them know we’ve detected unfamiliar activity on their account.

我们将向他们发送一个登录通知,让他们知道我们在他们的账户上发现了陌生的活动。

2. Users’ Location and Device Details

2.用户的位置和设备详细信息

There are two things we require: the locations of our users, and the information about the devices they use to log in.

我们需要两样东西:用户的位置,以及他们用来登录的设备的信息。

Considering that we’re using HTTP to exchange messages with our users, we’ll have to rely solely on the incoming HTTP request and its metadata to retrieve this information.

考虑到我们使用HTTP与用户交换信息,我们将不得不完全依靠传入的HTTP请求及其元数据来检索这些信息。

Luckily for us, there are HTTP headers whose sole purpose is to carry this kind of information.

幸运的是,有一些HTTP标头的唯一目的就是携带这类信息。

2.1. Device Location

2.1.设备位置

Before we can estimate our users’ location, we need to obtain their originating IP Address.

在我们能够估计用户的位置之前,我们需要获得他们的原生IP地址。

We can do that by using:

我们可以通过使用来做到这一点。

  • X-Forwarded-For – the de facto standard header for identifying the originating IP address of a client connecting to a web server through an HTTP proxy or load balancer
  • ServletRequest.getRemoteAddr() – a utility method that returns the originating IP of the client or the last proxy that sent the request

Extracting a user’s IP address from the HTTP request isn’t quite reliable since they may be tampered with. However, let’s simplify this in our tutorial and assume that won’t be the case.

从HTTP请求中提取用户的IP地址并不十分可靠,因为它们可能被篡改了。然而,在我们的教程中,让我们简化这个问题,假设不会出现这种情况。

Once we’ve retrieved the IP address, we can convert it to a real-world location through geolocation.

一旦我们检索到IP地址,我们就可以通过geolocation将其转换成现实世界的位置。

2.2. Device Details

2.2.设备详情

Similarly to the originating IP address, there’s also an HTTP header that carries information about the device that was used to send the request called User-Agent.

与发端IP地址类似,还有一个HTTP头,携带关于用来发送请求的设备的信息,称为User-Agent

In short, it carries information that allows us to identify the application typeoperating system, and software vendor/version of the requesting user agent.

简而言之,它携带的信息使我们能够识别应用程序类型操作系统软件供应商/版本请求用户代理

Here’s an example of what it may look like:

下面是一个可能看起来像什么的例子。

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

In our example above, the device is running on Mac OS X 10.14 and used Chrome 71.0 to send the request.

在我们上面的例子中,设备运行在Mac OS X 10.14并使用Chrome 71.0来发送请求。

Rather than implement a User-Agent parser from scratch, we’re going to resort to existing solutions that have already been tested and are more reliable.

与其从头开始实现一个用户代理解析器,我们不如求助于现有的解决方案,这些解决方案已经过测试,而且更可靠。

3. Detecting a New Device or Location

3.检测一个新的设备或位置

Now that we’ve introduced the information we need, let’s modify our AuthenticationSuccessHandler to perform validation after a user has logged in:

现在我们已经介绍了我们需要的信息,让我们修改我们的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);
        }
    }
    //...
}

We simply added a call to our new component: DeviceService. This component will encapsulate everything we need to identify new devices/locations and notify our users.

我们只是简单地添加了一个对我们新组件的调用。DeviceService。这个组件将封装我们所需要的一切,以识别新的设备/地点并通知我们的用户。

However, before we move onto our DeviceService, let’s create our DeviceMetadata entity to persist our users’ data over time:

然而,在我们转向我们的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;
    //...
}

And its Repository:

还有它的存储库

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

With our Entity and Repository in place, we can start gathering the information we need to keep a record of our users’ devices and their locations.

有了我们的EntityRepository,我们就可以开始收集我们需要的信息,以记录我们用户的设备和他们的位置。

4. Extracting Our User’s Location

4.提取我们用户的位置

Before we can estimate our user’s geographical location, we need to extract their IP address:

在我们能够估计用户的地理位置之前,我们需要提取他们的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;
}

If there’s an X-Forwarded-For header in the request, we’ll use it to extract their IP address; otherwise, we’ll use the getRemoteAddr() method.

如果请求中有一个X-Forwarded-For头,我们将使用它来提取他们的IP地址;否则,我们将使用getRemoteAddr()方法。

Once we have their IP address, we can estimate their location with the help of Maxmind:

一旦我们有了他们的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. Users’ Device Details

5. 用户 设备 细节

Since the User-Agent header contains all the information we need, it’s only a matter of extracting it. As we mentioned earlier, with the help of User-Agent parser (uap-java in this case), getting this information becomes quite simple:

由于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. Sending a Login Notification

6.发送登录通知

To send a login notification to our user, we need to compare the information we extracted against past data to check if we’ve already seen the device, in that location, in the past.

为了向用户发送登录通知,我们需要将我们提取的信息与过去的数据进行比较,以检查我们是否已经在过去的那个位置看到过这个设备。

Let’s take a look at our DeviceService.verifyDevice() method:

让我们看看我们的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);
    }
}

After extracting the information, we compare it against existing DeviceMetadata entries to check if there’s an entry containing the same information:

在提取信息后,我们将其与现有的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;
}

If there isn’t, we need to send a notification to our user to let them know that we’ve detected unfamiliar activity in their account. Then, we persist the information.

如果没有,我们需要向用户发送一个通知,让他们知道我们在他们的账户中发现了陌生的活动。然后,我们将信息持久化。

Otherwise, we simply update the lastLoggedIn attribute of the familiar device.

否则,我们只需更新熟悉设备的lastLoggedIn属性。

7. Conclusion

7.结论

In this article, we demonstrated how we can send a login notification in case we detect unfamiliar activity in users’ accounts.

在这篇文章中,我们演示了如何在检测到用户账户有陌生活动的情况下发送登录通知。

The full implementation of this tutorial can be found over on Github.

本教程的完整实现可以在Github上找到over