Рубрики
Без рубрики

Разрешить аутентификацию из принятых местоположений только с помощью Spring Security

Узнайте, как разрешить пользователям проходить аутентификацию только из принятых местоположений только с помощью Spring Security.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы сосредоточимся на очень интересной функции безопасности – защите учетной записи пользователя в зависимости от его местоположения.

Проще говоря, мы заблокируем любой вход в систему из необычных или нестандартных мест и позволим пользователю включить новые места безопасным способом.

Это часть серии регистрации и, естественно, строится поверх существующей кодовой базы.

2. Модель местоположения пользователя

Во – первых, давайте взглянем на нашу модель userLocation , которая содержит информацию о местоположениях входа пользователей; у каждого пользователя есть по крайней мере одно местоположение, связанное с его учетной записью:

@Entity
public class UserLocation {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String country;

    private boolean enabled;

    @ManyToOne(targetEntity = User.class, fetch = FetchType.EAGER)
    @JoinColumn(nullable = false, name = "user_id")
    private User user;

    public UserLocation() {
        super();
        enabled = false;
    }

    public UserLocation(String country, User user) {
        super();
        this.country = country;
        this.user = user;
        enabled = false;
    }
    ...
}

И мы собираемся добавить простую операцию извлечения в наш репозиторий:

public interface UserLocationRepository extends JpaRepository {
    UserLocation findByCountryAndUser(String country, User user);
}

Обратите внимание, что

  • Новое Расположение пользователя по умолчанию отключено
  • Каждый пользователь имеет по крайней мере одно местоположение, связанное с его учетными записями, которое является первым местоположением, в котором он получил доступ к приложению при регистрации

3. Регистрация

Теперь давайте обсудим, как изменить процесс регистрации, чтобы добавить местоположение пользователя по умолчанию:

@PostMapping("/user/registration")
public GenericResponse registerUserAccount(@Valid UserDto accountDto, 
  HttpServletRequest request) {
    
    User registered = userService.registerNewUserAccount(accountDto);
    userService.addUserLocation(registered, getClientIP(request));
    ...
}

При реализации сервиса мы получим страну по IP-адресу пользователя:

public void addUserLocation(User user, String ip) {
    InetAddress ipAddress = InetAddress.getByName(ip);
    String country 
      = databaseReader.country(ipAddress).getCountry().getName();
    UserLocation loc = new UserLocation(country, user);
    loc.setEnabled(true);
    loc = userLocationRepo.save(loc);
}

Обратите внимание, что мы используем базу данных GeoLite2 для получения страны по IP-адресу. Чтобы использовать GeoLite2 , нам нужна была зависимость maven:


    com.maxmind.geoip2
    geoip2
    2.9.0

И нам также нужно определить простой боб:

@Bean
public DatabaseReader databaseReader() throws IOException, GeoIp2Exception {
    File resource = new File("src/main/resources/GeoLite2-Country.mmdb");
    return new DatabaseReader.Builder(resource).build();
}

Мы загрузили базу данных GeoLite2 Country из MaxMind здесь.

4. Безопасный Вход в систему

Теперь, когда у нас есть страна пользователя по умолчанию, мы добавим простую проверку местоположения после аутентификации:

@Autowired
private DifferentLocationChecker differentLocationChecker;

@Bean
public DaoAuthenticationProvider authProvider() {
    CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(encoder());
    authProvider.setPostAuthenticationChecks(differentLocationChecker);
    return authProvider;
}

А вот наша Проверка другого местоположения :

@Component
public class DifferentLocationChecker implements UserDetailsChecker {

    @Autowired
    private IUserService userService;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void check(UserDetails userDetails) {
        String ip = getClientIP();
        NewLocationToken token = userService.isNewLoginLocation(userDetails.getUsername(), ip);
        if (token != null) {
            String appUrl = 
              "http://" 
              + request.getServerName() 
              + ":" + request.getServerPort() 
              + request.getContextPath();
            
            eventPublisher.publishEvent(
              new OnDifferentLocationLoginEvent(
                request.getLocale(), userDetails.getUsername(), ip, token, appUrl));
            throw new UnusualLocationException("unusual location");
        }
    }

    private String getClientIP() {
        String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            return request.getRemoteAddr();
        }
        return xfHeader.split(",")[0];
    }
}

Обратите внимание, что мы использовали s et PostAuthenticationChecks () , чтобы проверка выполнялась только после успешной аутентификации – когда пользователь предоставляет правильные учетные данные.

Кроме того, наше пользовательское Необычное исключение местоположения является простым AuthenticationException .

Нам также нужно будет изменить наш AuthenticationFailureHandler , чтобы настроить сообщение об ошибке:

@Override
public void onAuthenticationFailure(...) {
    ...
    else if (exception.getMessage().equalsIgnoreCase("unusual location")) {
        errorMessage = messages.getMessage("auth.message.unusual.location", null, locale);
    }
}

Теперь давайте внимательно рассмотрим реализацию is New Login Location() :

@Override
public NewLocationToken isNewLoginLocation(String username, String ip) {
    try {
        InetAddress ipAddress = InetAddress.getByName(ip);
        String country 
          = databaseReader.country(ipAddress).getCountry().getName();
        
        User user = repository.findByEmail(username);
        UserLocation loc = userLocationRepo.findByCountryAndUser(country, user);
        if ((loc == null) || !loc.isEnabled()) {
            return createNewLocationToken(country, user);
        }
    } catch (Exception e) {
        return null;
    }
    return null;
}

Обратите внимание, как, когда пользователь предоставляет правильные учетные данные, мы затем проверяем их местоположение. Если местоположение уже связано с этой учетной записью пользователя, пользователь может успешно пройти проверку подлинности.

Если нет, мы создаем NewLocationToken и отключенный userLocation – чтобы позволить пользователю включить это новое местоположение. Подробнее об этом в следующих разделах.

private NewLocationToken createNewLocationToken(String country, User user) {
    UserLocation loc = new UserLocation(country, user);
    loc = userLocationRepo.save(loc);
    NewLocationToken token = new NewLocationToken(UUID.randomUUID().toString(), loc);
    return newLocationTokenRepository.save(token);
}

Наконец, вот простая реализация NewLocationToken – чтобы пользователи могли связывать новые местоположения со своей учетной записью:

@Entity
public class NewLocationToken {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String token;

    @OneToOne(targetEntity = UserLocation.class, fetch = FetchType.EAGER)
    @JoinColumn(nullable = false, name = "user_location_id")
    private UserLocation userLocation;
    
    ...
}

5. Событие Входа В Другое Место

Когда пользователь входит в систему из другого местоположения, мы создали Новый токен местоположения и использовали его для запуска OnDifferentLocationLoginEvent :

public class OnDifferentLocationLoginEvent extends ApplicationEvent {
    private Locale locale;
    private String username;
    private String ip;
    private NewLocationToken token;
    private String appUrl;
}

Different Location LoginListener обрабатывает наше событие следующим образом:

@Component
public class DifferentLocationLoginListener 
  implements ApplicationListener {

    @Autowired
    private MessageSource messages;

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private Environment env;

    @Override
    public void onApplicationEvent(OnDifferentLocationLoginEvent event) {
        String enableLocUri = event.getAppUrl() + "/user/enableNewLoc?token=" 
          + event.getToken().getToken();
        String changePassUri = event.getAppUrl() + "/changePassword.html";
        String recipientAddress = event.getUsername();
        String subject = "Login attempt from different location";
        String message = messages.getMessage("message.differentLocation", new Object[] { 
          new Date().toString(), 
          event.getToken().getUserLocation().getCountry(), 
          event.getIp(), enableLocUri, changePassUri 
          }, event.getLocale());

        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(recipientAddress);
        email.setSubject(subject);
        email.setText(message);
        email.setFrom(env.getProperty("support.email"));
        mailSender.send(email);
    }
}

Обратите внимание, как когда пользователь входит в систему из другого места, мы отправим электронное письмо, чтобы уведомить его .

Если кто-то другой попытается войти в свою учетную запись, он, конечно, изменит свой пароль. Если они распознают попытку аутентификации, они смогут связать новое местоположение входа в систему со своей учетной записью.

6. Включите Новое местоположение входа в систему

Наконец, теперь, когда пользователь был уведомлен о подозрительной активности, давайте посмотрим, как приложение будет обрабатывать включение нового местоположения :

@RequestMapping(value = "/user/enableNewLoc", method = RequestMethod.GET)
public String enableNewLoc(Locale locale, Model model, @RequestParam("token") String token) {
    String loc = userService.isValidNewLocationToken(token);
    if (loc != null) {
        model.addAttribute(
          "message", 
          messages.getMessage("message.newLoc.enabled", new Object[] { loc }, locale)
        );
    } else {
        model.addAttribute(
          "message", 
          messages.getMessage("message.error", null, locale)
        );
    }
    return "redirect:/login?lang=" + locale.getLanguage();
}

И наш является допустимым новым маркером местоположения() метод:

@Override
public String isValidNewLocationToken(String token) {
    NewLocationToken locToken = newLocationTokenRepository.findByToken(token);
    if (locToken == null) {
        return null;
    }
    UserLocation userLoc = locToken.getUserLocation();
    userLoc.setEnabled(true);
    userLoc = userLocationRepo.save(userLoc);
    newLocationTokenRepository.delete(locToken);
    return userLoc.getCountry();
}

Проще говоря, мы включим Местоположение пользователя , связанное с токеном, а затем удалим токен.

7. Ограничения

Чтобы закончить статью, мы должны упомянуть об ограничении вышеупомянутой реализации. Метод, который мы использовали для определения IP-адреса клиента:

private final String getClientIP(HttpServletRequest request)

не всегда возвращает правильный IP-адрес клиента. Если приложение Spring Boot развернуто локально, возвращаемый IP-адрес (если не настроен иначе) равен 0.0.0.0. Поскольку этот адрес отсутствует в базе данных MaxMind, регистрация и вход в систему будут невозможны. Та же проблема возникает, если у клиента есть IP-адрес, которого нет в базе данных.

8. Заключение

В этом уроке мы сосредоточились на новом мощном механизме для повышения безопасности наших приложений – ограничении неожиданной активности пользователей в зависимости от их местоположения.

Как всегда, полную реализацию можно найти на Github .