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 .