Spring Securityでのみ利用可能な場所からの認証を許可

1概要

このチュートリアルでは、非常に興味深いセキュリティ機能、つまり場所に基づいてユーザーのアカウントを保護する方法に焦点を当てます。

簡単に言うと、** 私たちは異常な場所や標準外の場所からのログインをブロックし、ユーザーが安全な方法で新しい場所を有効にできるようにします。

これは 登録シリーズの一部 です。当然、既存のコードベースの上に構築されています。

2ユーザーロケーションモデル

まず、ユーザーのログイン場所に関する情報を保持する UserLocation モデルを見てみましょう。各ユーザーは、自分のアカウントに関連付けられた少なくとも1つの場所を持ちます。

@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, Long> {
    UserLocation findByCountryAndUser(String country, User user);
}

ご了承ください

  • 新しい UserLocation はデフォルトで無効になっています

  • 各ユーザーは、自分のアカウントに関連付けられた少なくとも1つの場所を持ちます。

登録時にアプリケーションにアクセスした最初の場所です。

3登録

それでは、登録プロセスを変更してデフォルトのユーザーの場所を追加する方法について説明しましょう。

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

サービスの実装では、ユーザーの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);
}

IPアドレスから国を取得するには、https://dev.maxmind.com/geoip/geoip2/geolite2/[GeoLite2]データベースを使用しています。 GeoLite2 を使用するには、Mavenの依存関係が必要です。

<dependency>
    <groupId>com.maxmind.geoip2</groupId>
    <artifactId>geoip2</artifactId>
    <version>2.9.0</version>
</dependency>

また、単純なBeanも定義する必要があります。

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

ここにMaxMindからhttp://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz[GeoLite2 Country]データベースをロードしました。

4安全なログイン

デフォルトのユーザーの国になったので、認証後に簡単な場所チェッカーを追加します。

@Autowired
private DifferentLocationChecker differentLocationChecker;

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

そして、これが私たちの DifferentLocationChecker です。

@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____etPostAuthenticationChecks()を使用したことに注意してください。

また、カスタムの UnusualLocationException は単純な AuthenticationException です。

エラーメッセージをカスタマイズするには、 AuthenticationFailureHandler を変更する必要もあります。

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

それでは、 isNewLoginLocation() 実装を詳しく見てみましょう。

@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 とdisabled 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別の場所へのログインイベント

ユーザーが別の場所からログインすると、 NewLocationToken が作成され、それを使用して OnDifferentLocationLoginEvent がトリガーされます。

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

DifferentLocationLoginListener は、このイベントを次のように処理します。

@Component
public class DifferentLocationLoginListener
  implements ApplicationListener<OnDifferentLocationLoginEvent> {

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

そして isValidNewLocationToken() メソッド:

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

簡単に言うと、トークンに関連付けられた UserLocation を有効にしてから、トークンを削除します。

7. 結論

このチュートリアルでは、アプリケーションにセキュリティを追加するための強力な新しいメカニズムに焦点を当てました。

いつものように、完全な実装はhttps://github.com/Baeldung/spring-security-registration[GiHubで]を見つけることができます。