1. 개요

이 빠른 예제에서는 Spring Security를 ​​사용하여 무차별 대입 인증 시도방지 하기위한 기본 솔루션을 구현합니다 .

간단히 말해서, 단일 IP 주소에서 발생한 실패한 시도 횟수를 기록합니다. 특정 IP가 설정된 요청 수를 초과하면 24 시간 동안 차단됩니다.

2. AuthenticationFailureEventListener

AuthenticationFailureEventListener 를 정의하여 시작해 보겠습니다. - AuthenticationFailureBadCredentialsEvent 이벤트 를 수신 하고 인증 실패를 알려줍니다.

@Component
public class AuthenticationFailureListener 
  implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails) 
          e.getAuthentication().getDetails();
        
        loginAttemptService.loginFailed(auth.getRemoteAddress());
    }
}

인증이 실패 하면 실패한 시도가 시작된 IP 주소를 LoginAttemptService알리는 방법에 유의하십시오 .

3. AuthenticationSuccessEventListener

또한 AuthenticationSuccessEventListener를 정의 해 보겠습니다. AuthenticationSuccessEvent 이벤트 를 수신하고 인증 성공을 알려줍니다.

@Component
public class AuthenticationSuccessEventListener 
  implements ApplicationListener<AuthenticationSuccessEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationSuccessEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails) 
          e.getAuthentication().getDetails();
        
        loginAttemptService.loginSucceeded(auth.getRemoteAddress());
    }
}

실패 리스너와 유사하게 인증 요청이 시작된 IP 주소를 LoginAttemptService알리는 방법에 유의 하십시오.

4. LoginAttemptService

이제 -LoginAttemptService 구현에 대해 논의 해 보겠습니다 . 간단히 말해, IP 주소 당 잘못된 시도 횟수를 24 시간 동안 유지합니다.

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Integer>() {
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void loginFailed(String key) {
        int attempts = 0;
        try {
            attempts = attemptsCache.get(key);
        } catch (ExecutionException e) {
            attempts = 0;
        }
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_ATTEMPT;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

방법 공지 사항 실패한 인증 시도가 해당 IP에 대한 시도의 수를 증가 하고, 성공적인 인증 재설정 카운터가.

이 시점부터는 인증 할 때 카운터확인하기 만하면 됩니다 .

5. UserDetailsService

이제 사용자 정의 UserDetailsService 구현 에 추가 검사를 추가하겠습니다 . 우리가로드 할 때 된 UserDetails를 , 이 IP 주소가 차단되면 우리는 먼저 필요가 확인 :

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private RoleRepository roleRepository;
 
    @Autowired
    private LoginAttemptService loginAttemptService;
 
    @Autowired
    private HttpServletRequest request;
 
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        String ip = getClientIP();
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("blocked");
        }
 
        try {
            User user = userRepository.findByEmail(email);
            if (user == null) {
                return new org.springframework.security.core.userdetails.User(
                  " ", " ", true, true, true, true, 
                  getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
            }
 
            return new org.springframework.security.core.userdetails.User(
              user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, 
              getAuthorities(user.getRoles()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

다음은 getClientIP () 메서드입니다.

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

클라이언트의 원래 IP 주소식별 하기 위한 몇 가지 추가 로직이 있습니다. 대부분의 경우 필요하지 않지만 일부 네트워크 시나리오에서는 필요합니다.

이러한 드문 시나리오의 경우 X-Forwarded-For 헤더를 사용하여 원래 IP에 도달합니다. 이 헤더의 구문은 다음과 같습니다.

X-Forwarded-For: clientIpAddress, proxy1, proxy2

또한 Spring이 가지고있는 또 다른 매우 흥미로운 기능에 주목 하십시오. HTTP 요청이 필요하므로 단순히 연결하는 것입니다.

이제 멋지다. 이것이 작동하려면 web.xml빠른 리스너를 추가 해야하며 작업이 훨씬 쉬워집니다.

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

그게 다 입니다. UserDetailsService 에서 요청에 액세스 할 수 있도록 web.xml 에이 새로운 RequestContextListener정의했습니다 .

6. AuthenticationFailureHandler 수정

마지막으로 CustomAuthenticationFailureHandler수정 하여 새 오류 메시지를 사용자 지정해 보겠습니다 .

사용자가 실제로 24 시간 동안 차단되는 상황을 처리하고 있으며 사용자에게 허용 된 잘못된 인증 시도가 허용 된 최대 값을 초과했기 때문에 IP가 차단되었음을 사용자에게 알립니다.

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Override
    public void onAuthenticationFailure(...) {
        ...

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);
        if (exception.getMessage().equalsIgnoreCase("blocked")) {
            errorMessage = messages.getMessage("auth.message.blocked", null, locale);
        }

        ...
    }
}

7. 결론

이것은 무차별 암호 대입 시도처리 하는 좋은 첫 번째 단계 이지만 개선의 여지가 있음 을 이해하는 것이 중요합니다 . 프로덕션 등급의 무차별 대입 방지 전략에는 IP가 차단하는 요소 이상이 포함될 수 있습니다.

이 예제 전체 구현github 프로젝트 에서 찾을 수 있습니다. 이것은 Eclipse 기반 프로젝트이므로 그대로 가져 와서 실행할 수 있어야합니다.