Skip to main content
 首页 » 编程设计

spring security防止恶意登录

2022年07月19日128jiqing9006

spring security防止恶意登录

本文我们使用spring security提供一个基本的解决方案防止恶意登录。
简单地说,我们记录来自同一IP的登录失败次数,如果超过设定的数量,在24小时内被阻止登录。

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

当认证失败时,我们通知LoginAttemptService 并传递尝试失败源Ip地址。

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()); 
    } 
} 
 
与登录失败监听器类似,我们通知LoginAttemptService并带上IP地址。 
 
## 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,登录失败时增加对应登录次数,成功登录重置次数。
因此,认证时,我们需要检查登录次数。

UserDetailsService

现在,我们需要在自定义的UserDetailService实现中增加额外检查,当加载UserDetail时,首先需要检查是否为阻止的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请求,因此,简单写在http请求里就行。

现在好了,我们需要快速注册http请求监听器,可以直接在web.xml中增加,从而简化应用开发:

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

javaConfig方式:

@Configuration 
@WebListener 
public class MyRequestContextListener extends RequestContextListener { 
} 

因为我们配置了RequestContextListener,所以在UserDetailService中可以直接访问request。

修改 AuthenticationFailureHandler

最后,我们修改自定义AuthenticationFailureHandler定制错误信息。
我们处理情节是当用户正好在被阻止登录的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); 
        } 
  
        ... 
    } 
} 

总结

这只是初步实现防止恶意登录,还有改进的空间,产品级的应用应该会涉及除了阻止ip外的更多信息。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/79825188