spring security防止恶意登录

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外的更多信息。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值