Medan Masuk Tambahan dengan Keselamatan Musim Semi

1. Pengenalan

Dalam artikel ini, kami akan menerapkan senario pengesahan tersuai dengan Spring Security dengan menambahkan medan tambahan pada borang log masuk standard .

Kami akan memfokuskan pada 2 pendekatan yang berbeza , untuk menunjukkan keragaman kerangka dan cara fleksibel yang dapat kami gunakan.

Pendekatan pertama kami adalah penyelesaian mudah yang memfokuskan pada penggunaan semula implementasi Spring Security inti yang ada.

Pendekatan kedua kami akan menjadi penyelesaian yang lebih khusus yang mungkin lebih sesuai untuk kes penggunaan lanjutan.

Kami akan membina konsep yang dibincangkan dalam artikel kami sebelumnya mengenai Spring Security login.

2. Persediaan Maven

Kami akan menggunakan permulaan Boot Musim Semi untuk memulakan projek kami dan membawa semua pergantungan yang diperlukan.

Persediaan yang akan kita gunakan memerlukan perisytiharan ibu bapa, pemula web, dan pemula keselamatan; kami juga akan memasukkan daun bidara:

 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE     org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-security   org.springframework.boot spring-boot-starter-thymeleaf   org.thymeleaf.extras thymeleaf-extras-springsecurity5  

Permulaan keselamatan Spring Boot versi terbaru boleh didapati di Maven Central.

3. Penyediaan Projek Ringkas

Dalam pendekatan pertama kami, kami akan menumpukan pada penggunaan semula pelaksanaan yang disediakan oleh Spring Security. Khususnya, kami akan menggunakan DaoAuthenticationProvider dan UsernamePasswordToken kerana ia wujud "out-of-the-box".

Komponen utama merangkumi:

  • SimpleAuthenticationFilter - lanjutan dari Nama PenggunaPasswordAuthenticationFilter
  • SimpleUserDetailsService - pelaksanaan UserDetailsService
  • Kami er - lanjutankelas Pengguna yang disediakan oleh Spring Security yang menyatakanbidang domain tambahan kami
  • Securi tyConfig - konfigurasi Spring Security kami yang memasukkan SimpleAuthenticationFilter kamike rantai penapis, menyatakan peraturan keselamatan dan membuat pergantungan
  • login.html - halaman log masuk yang mengumpulkan nama pengguna , kata laluan , dan domain

3.1. Penapis Pengesahan Mudah

Dalam SimpleAuthenticationFilter kami , bidang dan nama pengguna diekstrak dari permintaan . Kami menggabungkan nilai-nilai ini dan menggunakannya untuk membuat contoh Nama PenggunaPasswordAuthenticationToken .

Token kemudian diserahkan kepada AuthenticationProvider untuk pengesahan :

public class SimpleAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // ... UsernamePasswordAuthenticationToken authRequest = getAuthRequest(request); setDetails(request, authRequest); return this.getAuthenticationManager() .authenticate(authRequest); } private UsernamePasswordAuthenticationToken getAuthRequest( HttpServletRequest request) { String username = obtainUsername(request); String password = obtainPassword(request); String domain = obtainDomain(request); // ... String usernameDomain = String.format("%s%s%s", username.trim(), String.valueOf(Character.LINE_SEPARATOR), domain); return new UsernamePasswordAuthenticationToken( usernameDomain, password); } // other methods }

3.2. Mudah UserDetails Perkhidmatan

The UserDetailsService kontrak mentakrifkan kaedah tunggal dipanggil loadUserByUsername. Pelaksanaan kami mengekstrak nama pengguna dan domain. Nilai kemudian diteruskan ke serRepository U kami untuk mendapatkan Pengguna :

public class SimpleUserDetailsService implements UserDetailsService { // ... @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String[] usernameAndDomain = StringUtils.split( username, String.valueOf(Character.LINE_SEPARATOR)); if (usernameAndDomain == null || usernameAndDomain.length != 2) { throw new UsernameNotFoundException("Username and domain must be provided"); } User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]); if (user == null) { throw new UsernameNotFoundException( String.format("Username not found for domain, username=%s, domain=%s", usernameAndDomain[0], usernameAndDomain[1])); } return user; } } 

3.3. Konfigurasi Keselamatan Musim Bunga

Persediaan kami berbeza dari konfigurasi Spring Security standard kerana kami memasukkan SimpleAuthenticationFilter ke rantai penapis sebelum lalai dengan panggilan untuk menambahFilterSebelum :

@Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/css/**", "/index").permitAll() .antMatchers("/user/**").authenticated() .and() .formLogin().loginPage("/login") .and() .logout() .logoutUrl("/logout"); }

Kami dapat menggunakan DaoAuthenticationProvider yang disediakan kerana kami mengkonfigurasinya dengan SimpleUserDetailsService kami . Ingatlah bahawa SimpleUserDetailsService kami tahu bagaimana menguraikan bidang nama pengguna dan domain kami dan mengembalikan Pengguna yang sesuai untuk digunakan semasa mengesahkan:

public AuthenticationProvider authProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); return provider; } 

Oleh kerana kami menggunakan SimpleAuthenticationFilter , kami mengkonfigurasi AuthenticationFailureHandler kami sendiri untuk memastikan percubaan log masuk yang gagal ditangani dengan tepat:

public SimpleAuthenticationFilter authenticationFilter() throws Exception { SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); filter.setAuthenticationFailureHandler(failureHandler()); return filter; }

3.4. Halaman Log Masuk

Halaman log masuk yang kami gunakan mengumpulkan medan domain tambahan kami yang diekstrak oleh SimpleAuthenticationFilter kami :

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Semasa kami menjalankan aplikasi dan mengakses konteks di // localhost: 8081, kami melihat pautan untuk mengakses halaman yang dilindungi. Mengklik pautan akan menyebabkan halaman masuk dipaparkan. Seperti yang dijangkakan, kami melihat bidang domain tambahan :

3.5. Ringkasan

Dalam contoh pertama kami, kami dapat menggunakan semula DaoAuthenticationProvider dan UsernamePasswordAuthenticationToken dengan "memalsukan" medan nama pengguna.

Hasilnya, kami dapat menambahkan sokongan untuk bidang log masuk tambahan dengan sedikit konfigurasi dan kod tambahan .

4. Penyediaan Projek Tersuai

Pendekatan kedua kami akan sangat serupa dengan yang pertama tetapi mungkin lebih sesuai untuk kes penggunaan yang tidak remeh.

Komponen utama pendekatan kedua kami merangkumi:

  • CustomAuthenticationFilter - lanjutan dari Nama PenggunaPasswordAuthenticationFilter
  • CustomUserDetailsService - antara muka khusus yang menyatakankaedah loadUserbyUsernameAndDomain
  • CustomUserDetailsServiceImpl - pelaksanaan CustomUserDetailsService kami
  • CustomUserDetailsAuthenticationProvider - lanjutan dari AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken - lanjutan dari Nama PenggunaPasswordAuthenticationToken
  • Kami er - lanjutankelas Pengguna yang disediakan oleh Spring Security yang menyatakanbidang domain tambahan kami
  • Securi tyConfig - konfigurasi Spring Security kami yang memasukkan CustomAuthenticationFilter kamike rantai penapis, menyatakan peraturan keselamatan dan membuat pergantungan
  • login.html - halaman log masuk yang mengumpulkan nama pengguna , kata laluan , dan domain

4.1. Penapis Pengesahan Tersuai

Dalam CustomAuthenticationFilter kami , kami mengekstrak nama pengguna, kata laluan, dan bidang domain dari permintaan . Nilai-nilai ini digunakan untuk membuat contoh Custom AuthenticationToken kami yang diteruskan ke AuthenticationProvider untuk pengesahan:

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain"; @Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // ... CustomAuthenticationToken authRequest = getAuthRequest(request); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) { String username = obtainUsername(request); String password = obtainPassword(request); String domain = obtainDomain(request); // ... return new CustomAuthenticationToken(username, password, domain); }

4.2. Custom UserDetails Perkhidmatan

Kontrak CustomUserDetailsService kami mentakrifkan satu kaedah yang dipanggil loadUserByUsernameAndDomain.

The CustomUserDetailsServiceImpl kelas kita mewujudkan hanya melaksanakan kontrak dan perwakilan untuk kami CustomUserRepository untuk mendapatkan pengguna :

 public UserDetails loadUserByUsernameAndDomain(String username, String domain) throws UsernameNotFoundException { if (StringUtils.isAnyBlank(username, domain)) { throw new UsernameNotFoundException("Username and domain must be provided"); } User user = userRepository.findUser(username, domain); if (user == null) { throw new UsernameNotFoundException( String.format("Username not found for domain, username=%s, domain=%s", username, domain)); } return user; }

4.3. Custom UserDetailsAuthenticationProvider

Our CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider and delegates to our CustomUserDetailService to retrieve the User. The most important feature of this class is the implementation of the retrieveUser method.

Note that we must cast the authentication token to our CustomAuthenticationToken for access to our custom field:

@Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication; UserDetails loadedUser; try { loadedUser = this.userDetailsService .loadUserByUsernameAndDomain(auth.getPrincipal() .toString(), auth.getDomain()); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials() .toString(); passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException( repositoryProblem.getMessage(), repositoryProblem); } // ... return loadedUser; }

4.4. Summary

Our second approach is nearly identical to the simple approach we presented first. By implementing our own AuthenticationProvider and CustomAuthenticationToken, we avoided needing to adapt our username field with custom parsing logic.

5. Conclusion

In this article, we've implemented a form login in Spring Security that made use of an extra login field. We did this in 2 different ways:

  • In our simple approach, we minimized the amount of code we needed write. We were able to reuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username with custom parsing logic
  • Dalam pendekatan kami yang lebih disesuaikan, kami memberikan sokongan bidang khusus dengan memperluas AbstractUserDetailsAuthenticationProvider dan menyediakan CustomUserDetailsService kami sendiri dengan CustomAuthenticationToken

Seperti biasa, semua kod sumber boleh didapati di GitHub.