Field Login Ekstra dengan Keamanan Musim Semi

1. Perkenalan

Di artikel ini, kami akan menerapkan skenario otentikasi khusus dengan Keamanan Musim Semi dengan menambahkan bidang tambahan ke formulir masuk standar .

Kami akan fokus pada 2 pendekatan berbeda , untuk menunjukkan keserbagunaan kerangka kerja dan cara fleksibel untuk menggunakannya.

Pendekatan pertama kami akan menjadi solusi sederhana yang berfokus pada penggunaan kembali implementasi Keamanan Musim Semi inti yang ada.

Pendekatan kedua kami akan menjadi solusi yang lebih khusus yang mungkin lebih cocok untuk kasus penggunaan lanjutan.

Kami akan membangun di atas konsep yang dibahas di artikel kami sebelumnya tentang login Keamanan Musim Semi.

2. Pengaturan Maven

Kami akan menggunakan permulaan Spring Boot untuk mem-bootstrap proyek kami dan memasukkan semua dependensi yang diperlukan.

Setup yang akan kita gunakan membutuhkan deklarasi induk, web starter, dan security starter; kami juga akan menyertakan thymeleaf:

 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  

Versi terbaru dari starter keamanan Spring Boot dapat ditemukan di Maven Central.

3. Setup Proyek Sederhana

Dalam pendekatan pertama kami, kami akan fokus pada penggunaan kembali implementasi yang disediakan oleh Spring Security. Secara khusus, kami akan menggunakan kembali DaoAuthenticationProvider dan UsernamePasswordToken karena keduanya ada "di luar kotak".

Komponen utama meliputi:

  • SimpleAuthenticationFilter - ekstensi dari UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService - implementasi dari UserDetailsService
  • Us er - ekstensi darikelas Pengguna yang disediakan oleh Spring Security yang menyatakanbidang domain ekstra kami
  • Securi tyConfig - konfigurasi Keamanan Musim Semi kami yang menyisipkan SimpleAuthenticationFilter kamike dalam rantai filter, menyatakan aturan keamanan, dan menghubungkan ketergantungan
  • login.html - halaman login yang mengumpulkan nama pengguna , kata sandi , dan domain

3.1. Filter Otentikasi Sederhana

Di SimpleAuthenticationFilter kami , bidang domain dan nama pengguna diekstrak dari permintaan . Kami menggabungkan nilai-nilai ini dan menggunakannya untuk membuat instance UsernamePasswordAuthenticationToken .

Token tersebut kemudian diteruskan ke AuthenticationProvider untuk otentikasi :

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. Layanan UserDetails Sederhana

The UserDetailsService kontrak mendefinisikan sebuah metode tunggal yang disebut loadUserByUsername. Implementasi kami mengekstrak nama pengguna dan domain. Nilai-nilai tersebut kemudian diteruskan ke U serRepository 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 Keamanan Musim Semi

Penyiapan kami berbeda dari konfigurasi Keamanan Musim Semi standar karena kami memasukkan SimpleAuthenticationFilter ke dalam rantai filter sebelum default dengan panggilan ke addFilterBefore :

@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 karena kami mengonfigurasinya dengan SimpleUserDetailsService kami . Ingatlah bahwa SimpleUserDetailsService kami tahu cara mengurai bidang nama pengguna dan domain kami dan mengembalikan Pengguna yang sesuai untuk digunakan saat mengautentikasi:

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

Karena kami menggunakan SimpleAuthenticationFilter , kami mengonfigurasi AuthenticationFailureHandler kami sendiri untuk memastikan upaya login 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 masuk

Halaman login yang kami gunakan mengumpulkan bidang domain tambahan kami yang diekstraksi oleh SimpleAuthenticationFilter kami :

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Ketika kami menjalankan aplikasi dan mengakses konteks di // localhost: 8081, kami melihat tautan untuk mengakses halaman aman. Mengklik link tersebut akan menampilkan halaman login. Seperti yang diharapkan, kami melihat bidang domain tambahan :

3.5. Ringkasan

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

Hasilnya, kami dapat menambahkan dukungan untuk bidang login tambahan dengan sedikit konfigurasi dan kode tambahan .

4. Pengaturan Proyek Kustom

Pendekatan kedua kami akan sangat mirip dengan yang pertama tetapi mungkin lebih sesuai untuk kasus penggunaan non-sepele.

Komponen kunci dari pendekatan kedua kami akan mencakup:

  • CustomAuthenticationFilter - ekstensi dari UsernamePasswordAuthenticationFilter
  • CustomUserDetailsService - antarmuka khusus yang mendeklarasikanmetode loadUserbyUsernameAndDomain
  • CustomUserDetailsServiceImpl - implementasi dari CustomUserDetailsService kami
  • CustomUserDetailsAuthenticationProvider - ekstensi dari AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken - ekstensi dari UsernamePasswordAuthenticationToken
  • Us er - ekstensi darikelas Pengguna yang disediakan oleh Spring Security yang menyatakanbidang domain ekstra kami
  • Securi tyConfig - konfigurasi Keamanan Musim Semi kami yang menyisipkan CustomAuthenticationFilter ke dalam rantai filter, menyatakan aturan keamanan, dan menghubungkan ketergantungan
  • login.html - halaman login yang mengumpulkan nama pengguna , kata sandi , dan domain

4.1. Filter Otentikasi Kustom

Di CustomAuthenticationFilter , kami mengekstrak bidang nama pengguna, kata sandi, dan domain dari permintaan . Nilai-nilai ini digunakan untuk membuat instance dari Custom AuthenticationToken kami yang diteruskan ke AuthenticationProvider untuk otentikasi:

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. Layanan UserDetails Kustom

Kontrak CustomUserDetailsService kami menentukan metode tunggal yang disebut loadUserByUsernameAndDomain.

Kelas CustomUserDetailsServiceImpl yang kita buat hanya mengimplementasikan kontrak dan mendelegasikannya ke 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 menyediakan dukungan bidang khusus dengan memperluas AbstractUserDetailsAuthenticationProvider dan menyediakan CustomUserDetailsService kami sendiri dengan CustomAuthenticationToken

Seperti biasa, semua kode sumber dapat ditemukan di GitHub.