转载

用 spring-boot 實作預防暴力登入嘗試的機制

最近在研究 spting-boot 框架和一些 web 開發的基礎安全性問題,花了點時間了解如何實作預防暴力登入的機制。
主要參考這篇文章 Prevent Brute Force Authentication Attempts with Spring Security 。

該篇文章將驗證 request ip 寫在 UserDetailsService 中,而官網對該 interface 的定義為 "Core interface which loads user-specific data"。相較之下我覺得放在提供驗證方法的 AuthenticationProvider (官網定義為 “Indicates a class can process a specific Authentication implementation” )更合適。下面簡介如實作:

LoginAttemptService

LoginAttemptService 提供一個存取登入失敗次數和對應 ip 列表的服務,利用 guava 的 LoadingCache 存取 block list並設定 timeout,藉此實作 block time out 的機制。

@Service public class LoginAttemptService {      @Autowired     private HttpServletRequest request;     private final int MAX_ATTEMPT = 2;     private final int bolckTimeMins = 1;     private LoadingCache<String, Integer> attemptsCache;      public LoginAttemptService() {         attemptsCache = CacheBuilder.newBuilder().                 expireAfterWrite(bolckTimeMins, TimeUnit.MINUTES).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;         }     } }

AuthenticationSuccessEventListener

一個監聽登入成功事件的 listener,每當用戶登入成功便透過 LoginAttemptService 將該 ip 從 block list 中清除。

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

AuthenticationFailureListener

一個監聽登入失敗事件的 listener,每當用戶登入失敗就透過 LoginAttemptService 將該 ip 放入 block list 中,並記錄失敗次數。

@Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {      @Override     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,                                         AuthenticationException exception) throws IOException {         response.sendError(403, exception.getMessage());     } }

MyUserDetailsService

實作一個 UserDetailsService,透過 Spring Data Repositories 讀取使用者資料。

@Service("MyUserDetailsImpl") public class MyUserDetailsService implements UserDetailsService {     @Autowired     private UserRepository repo;      public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {         User user;         try {             user = repo.getByUsername(userName);         } catch (Exception e) {             throw new UsernameNotFoundException("user select fail");         }         if(user == null){             throw new UsernameNotFoundException("no user found");         } else {             try {                 List<GrantedAuthority> gas = new ArrayList<GrantedAuthority>();                 gas.add(new SimpleGrantedAuthority("ROLE_USER"));                 return new org.springframework.security.core.userdetails.User(                         user.getUsername(), user.getPassword(), true, true, true, true, gas);             } catch (Exception e) {                 throw new UsernameNotFoundException("user role select fail");             }         }     } }

MyAuthenticationProvider

實作一個 AuthenticationProvider ,在驗證帳號密碼之前,會先透過 LoginAttemptService 確認該 request 的 ip 是否被 block 。

@Component public class MyAuthenticationProvider implements AuthenticationProvider {     @Autowired     private MyUserDetailsService myUserDetailsService;     @Autowired     private LoginAttemptService loginAttemptService;     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         WebAuthenticationDetails wad = (WebAuthenticationDetails) authentication.getDetails();         String userIPAddress = wad.getRemoteAddress();         String username = authentication.getName();         String password = (String) authentication.getCredentials();         if(loginAttemptService.isBlocked(userIPAddress)) {             throw new LockedException("This ip has been blocked");         }         UserDetails user = myUserDetailsService.loadUserByUsername(username);         if(user == null){             throw new BadCredentialsException("Username not found.");         }         if (!Password.encoder.matches(password, user.getPassword())) {             throw new BadCredentialsException("Wrong password.");         }          Collection<? extends GrantedAuthority> authorities = user.getAuthorities();         return new UsernamePasswordAuthenticationToken(user, password, authorities);     }      public boolean supports(Class<?> authentication) {         return true;     } }

SimpleUrlAuthenticationFailureHandler

實作處理驗證失敗的後續行為,此次範例僅簡單拋回 403 異常。

@Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {      @Override     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,                                         AuthenticationException exception) throws IOException {         response.sendError(403, exception.getMessage());     } }

在 WebSecurityConfigurerAdapter 中設定認證時使用自定義的 MyUserDetailsService 、 MyAuthentcationProvider 和 MyAuthenticationFailureHandler

@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Autowired     DataSource dataSource;     @Autowired     private UserRepository _userRepo;     @Autowired     private MyUserDetailsService myUserDetailsService;     @Autowired     private MyAuthenticationProvider myAuthenticationProvider;     @Autowired     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;      @Override     protected void configure(HttpSecurity http) throws Exception {         http                 .authorizeRequests()                 .antMatchers("/", "/home", "/signin", "/index").permitAll()                 .anyRequest().authenticated()                 .and()                 .formLogin()                 .loginPage("/login")                 .permitAll()                 .failureHandler(myAuthenticationFailureHandler)                 .and()                 .logout()                 .permitAll()                 .and()                 .csrf().disable();     }      @Override     public UserDetailsService userDetailsServiceBean() throws Exception {         return myUserDetailsService;     }      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(myUserDetailsService);         auth.authenticationProvider(myAuthenticationProvider);     } }

總結

大功告成!事實上只要了解如何自定義認證過程,細節部分還需要是使用情境修改,例如 block list 是要存在 memory 裡面還是要存到外部 db;用 ip 當作 block list 的 key 還要用 username + ip 等等。
完整的 code 可以參考 github 。

原文  http://genchilu-blog.logdown.com/posts/745182
正文到此结束
Loading...