Authentifizierung von akzeptierten Standorten nur mit Spring Security zulassen

Authentifizierung von akzeptierten Standorten nur mit Spring Security zulassen

1. Überblick

In diesem Tutorial konzentrieren wir uns auf eine sehr interessante Sicherheitsfunktion - das Sichern des Kontos eines Benutzers anhand seines Standorts.

Einfach ausgedrückt,we’ll block any login from unusual or non-standard locations und ermöglichen dem Benutzer, neue Standorte auf sichere Weise zu aktivieren.

Dies istpart of the registration series und baut natürlich auf der vorhandenen Codebasis auf.

2. Benutzerstandortmodell

Schauen wir uns zunächst dasUserLocation-Modell an, das Informationen zu den Anmeldeorten der Benutzer enthält. Jedem Benutzer ist mindestens ein Standort mit seinem Konto verknüpft:

@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;
    }
    ...
}

Und wir werden unserem Repository einen einfachen Abrufvorgang hinzufügen:

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

Beachten Sie, dass

  • Das neueUserLocation ist standardmäßig deaktiviert

  • Jeder Benutzer hat mindestens einen Standort, der seinen Konten zugeordnet ist. Dies ist der erste Standort, an dem er bei der Registrierung auf die Anwendung zugegriffen hat

3. Anmeldung

Lassen Sie uns nun erläutern, wie Sie den Registrierungsprozess ändern, um den Standardbenutzerspeicherort hinzuzufügen:

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
@ResponseBody
public GenericResponse registerUserAccount(@Valid UserDto accountDto,
  HttpServletRequest request) {

    User registered = userService.registerNewUserAccount(accountDto);
    userService.addUserLocation(registered, getClientIP(request));
    ...
}

Bei der Service-Implementierung erhalten wir das Land anhand der IP-Adresse des Benutzers:

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);
}

Beachten Sie, dass wir die DatenbankGeoLite2verwenden, um das Land von der IP-Adresse abzurufen. UmGeoLite2 zu verwenden, brauchten wir die Maven-Abhängigkeit:


    com.maxmind.geoip2
    geoip2
    2.9.0

Und wir müssen auch eine einfache Bohne definieren:

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

Wir haben hier dieGeoLite2 Country-Datenbank von MaxMind geladen.

4. Sicherheits Login

Nachdem wir das Standardland des Benutzers haben, fügen wir nach der Authentifizierung eine einfache Standortprüfung hinzu:

@Autowired
private DifferentLocationChecker differentLocationChecker;

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

Und hier sind unsereDifferentLocationChecker:

@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];
    }
}

Beachten Sie, dass wir setPostAuthenticationChecks() verwendet haben, damitthe check only run after successful authentication - wenn der Benutzer die richtigen Anmeldeinformationen angibt.

Außerdem ist unser benutzerdefiniertesUnusualLocationException ein einfachesAuthenticationException.

Wir müssen auch unsereAuthenticationFailureHandlerändern, um die Fehlermeldung anzupassen:

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

Schauen wir uns nun die Implementierung vonisNewLoginLocation()genauer an:

@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;
}

Beachten Sie, dass wir dann, wenn der Benutzer die korrekten Anmeldeinformationen eingibt, deren Position überprüfen. Wenn der Speicherort bereits mit diesem Benutzerkonto verknüpft ist, kann sich der Benutzer erfolgreich authentifizieren.

Wenn nicht, erstellen wirNewLocationToken undUserLocation deaktiviert, damit der Benutzer diesen neuen Speicherort aktivieren kann. Mehr dazu in den folgenden Abschnitten.

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);
}

Schließlich ist hier die einfache Implementierung vonNewLocationToken, damit Benutzer ihrem Konto neue Standorte zuordnen können:

@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. Anmeldeereignis für einen anderen Standort

Wenn sich der Benutzer von einem anderen Ort aus anmeldet, haben wir einNewLocationToken erstellt und damit einOnDifferentLocationLoginEvent ausgelöst:

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

DasDifferentLocationLoginListener behandelt unser Ereignis wie folgt:

@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);
    }
}

Beachten Sie, wiewhen the user logs in from a different location, we’ll send an email to notify them.

Wenn jemand anderes versucht hat, sich in sein Konto einzuloggen, ändert er natürlich sein Passwort. Wenn sie den Authentifizierungsversuch erkennen, können sie den neuen Anmeldeort ihrem Konto zuordnen.

6. Aktivieren Sie einen neuen Anmeldeort

Nachdem der Benutzer über die verdächtige Aktivität informiert wurde, werfen wir einen Blick aufhow the application will handle enabling the new location:

@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();
}

Und unsereisValidNewLocationToken()-Methode:

@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();
}

Einfach ausgedrückt, aktivieren wir die mit dem Token verknüpftenUserLocationund löschen das Token.

7. Fazit

In diesem Tutorial haben wir uns auf einen leistungsstarken neuen Mechanismus konzentriert, um unseren Anwendungen Sicherheit zu verleihen -restricting unexpected user activity based on their location.

Wie immer kann die vollständige Implementierungover on GiHub gefunden werden.