Ya, ini mungkin. Anda bahkan dapat lebih mengamankan aplikasi web Anda dengan memvalidasi bidang CN pada sertifikat dan memblokirnya jika nama sertifikat tersebut tidak benar. Saya tidak yakin apakah hal ini dapat dilakukan dengan Spring Security, tetapi saya tahu hal ini dapat dilakukan dengan AOP dengan menggunakan AspectJ. Dengan cara ini Anda dapat mencegat permintaan setelah jabat tangan SSL berhasil dan sebelum permintaan tersebut masuk ke pengontrol Anda. Saya sangat menyarankan untuk membaca artikel ini: Pengantar AspectJ karena ini akan membantu Anda memahami konsep dasar dari perpustakaan.
Yang bisa Anda lakukan adalah membuat anotasi, misalnya: ExtraCertificateValidations yang dapat mengambil daftar nama umum yang diperbolehkan dan tidak diperbolehkan. Lihat di bawah untuk implementasinya. Dengan cara ini Anda dapat memutuskan pada setiap pengontrol CN mana yang ingin Anda izinkan dan tidak izinkan.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdditionalCertificateValidations {
String[] allowedCommonNames() default {};
String[] notAllowedCommonNames() default {};
}
Kata penutup, Anda dapat memberi anotasi pada pengontrol Anda dengan anotasi di atas dan menentukan nama umum:
@Controller
public class HelloWorldController {
@AdditionalCertificateValidations(allowedCommonNames = {"my-common-name-a", "my-common-name-b"}, notAllowedCommonNames = {"my-common-name-c"})
@GetMapping(value = "/api/hello", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello");
}
}
Sekarang Anda perlu menyediakan implementasi untuk anotasi tersebut. Kelas sebenarnya yang akan mencegat permintaan dan juga memvalidasi konten sertifikat.
@Aspect
@Configuration
@EnableAspectJAutoProxy
public class AdditionalCertificateValidationsAspect {
private static final String KEY_CERTIFICATE_ATTRIBUTE = "javax.servlet.request.X509Certificate";
private static final Pattern COMMON_NAME_PATTERN = Pattern.compile("(?<=CN=)(.*?)(?=,)");
@Around("@annotation(certificateValidations)")
public Object validate(ProceedingJoinPoint joinPoint,
AdditionalCertificateValidations certificateValidations) throws Throwable {
List<String> allowedCommonNames = Arrays.asList(certificateValidations.allowedCommonNames());
List<String> notAllowedCommonNames = Arrays.asList(certificateValidations.notAllowedCommonNames());
Optional<String> allowedCommonName = getCommonNameFromCertificate()
.filter(commonName -> allowedCommonNames.isEmpty() || allowedCommonNames.contains(commonName))
.filter(commonName -> notAllowedCommonNames.isEmpty() || !notAllowedCommonNames.contains(commonName));
if (allowedCommonName.isPresent()) {
return joinPoint.proceed();
} else {
return ResponseEntity.badRequest().body("This certificate is not a valid one");
}
}
private Optional<String> getCommonNameFromCertificate() {
return getCertificatesFromRequest()
.map(Arrays::stream)
.flatMap(Stream::findFirst)
.map(X509Certificate::getSubjectX500Principal)
.map(X500Principal::getName)
.flatMap(this::getCommonName);
}
private Optional<X509Certificate[]> getCertificatesFromRequest() {
return Optional.ofNullable((X509Certificate[]) ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest()
.getAttribute(KEY_CERTIFICATE_ATTRIBUTE));
}
private Optional<String> getCommonName(String subjectDistinguishedName) {
Matcher matcher = COMMON_NAME_PATTERN.matcher(subjectDistinguishedName);
if (matcher.find()) {
return Optional.of(matcher.group());
} else {
return Optional.empty();
}
}
}
Dengan konfigurasi di atas klien dengan nama umum yang diperbolehkan akan mendapatkan kode status 200 dengan pesan hello dan klien lain akan mendapatkan kode status 400 dengan pesan: Sertifikat ini tidak valid. Anda dapat menggunakan opsi di atas dengan perpustakaan tambahan berikut:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
Contoh proyek dapat ditemukan di sini: GitHub - Mutual-TLS-SSL
Cuplikan kode contoh dapat ditemukan di sini:
=============== perbarui 1#
Saya menemukan bahwa nama CN juga dapat divalidasi hanya dengan keamanan pegas. Lihat penjelasan detailnya beserta contohnya di sini: https://www.baeldung.com/x-509-authentication-in-spring-security#2-spring-security-configuration
Pertama, Anda perlu memberi tahu pegas untuk mencegat setiap permintaan, mengotorisasi, dan mengautentikasi dengan mengganti metode configure
dengan logika Anda sendiri, lihat contoh di bawah. Ini akan mengekstrak bidang nama umum dan memperlakukannya sebagai Nama Pengguna dan akan memeriksa dengan UserDetailsService apakah pengguna tersebut dikenal. Pengontrol Anda juga perlu dianotasi dengan @PreAuthorize("hasAuthority('ROLE_USER')")
@SpringBootApplication
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class X509AuthenticationServer extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService());
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) {
if (username.equals("Bob")) {
return new User(username, "",
AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
throw new UsernameNotFoundException("User not found!");
}
};
}
}
=============== perbarui 2#
Saya entah bagaimana melewatkan poin yang seharusnya dilakukan dengan cara yang tidak menghalangi. Aliran reaktif agak mirip dengan contoh yang diberikan pada pembaruan pertama di atas. Konfigurasi berikut akan membantu Anda:
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.x509(Customizer.withDefaults())
.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
.build();
}
@Bean
public MapReactiveUserDetailsService mapReactiveUserDetailsService() {
UserDetails bob = User.withUsername("Bob")
.authorities(new SimpleGrantedAuthority("ROLE_USER"))
.password("")
.build();
return new MapReactiveUserDetailsService(bob);
}
Saya membuat contoh implementasi yang berfungsi berdasarkan masukan di atas, lihat di sini untuk detailnya: GitHub - Keamanan pegas dengan validasi nama umum
person
Hakan54
schedule
30.09.2020