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