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 inBack 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.