ใช่สิ่งนี้เป็นไปได้ คุณยังสามารถรักษาความปลอดภัยแอปพลิเคชันเว็บของคุณเพิ่มเติมได้ด้วยการตรวจสอบฟิลด์ CN ของใบรับรอง และบล็อกใบรับรองหากไม่มีชื่อที่ถูกต้อง ฉันไม่แน่ใจว่าจะเป็นไปได้หรือไม่เมื่อใช้ Spring Security ทันที แต่ฉันรู้ว่า AOP สามารถทำได้โดยใช้ AspectJ ด้วยวิธีนี้ คุณสามารถสกัดกั้นคำขอได้หลังจากการแฮนด์เชค SSL สำเร็จ และก่อนที่จะเข้าสู่คอนโทรลเลอร์ของคุณ ฉันขอแนะนำให้อ่านบทความนี้อย่างแน่นอน: ข้อมูลเบื้องต้นเกี่ยวกับ AspectJ เนื่องจากจะช่วยให้คุณเข้าใจแนวคิดพื้นฐาน ของห้องสมุด
สิ่งที่คุณสามารถทำได้คือสร้างคำอธิบายประกอบ เช่น ExtraCertificateValidations ซึ่งสามารถรับรายการชื่อสามัญที่ได้รับอนุญาตและไม่ได้รับอนุญาต ดูด้านล่างสำหรับการนำไปใช้งาน ด้วยวิธีนี้ คุณสามารถตัดสินใจเลือกคอนโทรลเลอร์ทุกตัวได้ว่า CN ใดที่คุณต้องการอนุญาตและไม่อนุญาต
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdditionalCertificateValidations {
String[] allowedCommonNames() default {};
String[] notAllowedCommonNames() default {};
}
Afterwords คุณสามารถใส่คำอธิบายประกอบคอนโทรลเลอร์ของคุณด้วยคำอธิบายประกอบข้างต้นและระบุชื่อทั่วไป:
@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");
}
}
ตอนนี้คุณต้องจัดเตรียมการใช้งานสำหรับคำอธิบายประกอบ คลาสจริงที่จะดักฟังคำขอและตรวจสอบเนื้อหาใบรับรองด้วย
@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();
}
}
}
ด้วยการกำหนดค่าข้างต้น ไคลเอนต์ที่มีชื่อทั่วไปที่อนุญาตจะได้รับรหัสสถานะ 200 พร้อมข้อความสวัสดี และไคลเอนต์อื่น ๆ จะได้รับรหัสสถานะ 400 พร้อมข้อความ: ใบรับรองนี้ไม่ถูกต้อง คุณสามารถใช้ตัวเลือกข้างต้นกับไลบรารีเพิ่มเติมต่อไปนี้:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
สามารถดูโครงการตัวอย่างได้ที่นี่: GitHub - Mutual-TLS-SSL
คุณสามารถดูตัวอย่างโค้ดได้ที่นี่:
=============== อัปเดต 1#
ฉันค้นพบว่าชื่อ CN สามารถตรวจสอบได้ด้วยการรักษาความปลอดภัยแบบสปริงเท่านั้น ดูคำอธิบายโดยละเอียดพร้อมตัวอย่างได้ที่นี่: https://www.baeldung.com/x-509-authentication-in-spring-security#2-spring-security-configuration
ขั้นแรก คุณต้องบอกให้ Spring ดักทุกคำขอ อนุญาตและรับรองความถูกต้องโดยการแทนที่เมธอด configure
ด้วยตรรกะของคุณเอง ดูตัวอย่างด้านล่าง มันจะแยกฟิลด์ชื่อสามัญและถือเป็นชื่อผู้ใช้และจะตรวจสอบกับ UserDetailsService หากรู้จักผู้ใช้ คอนโทรลเลอร์ของคุณต้องมีคำอธิบายประกอบด้วย @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!");
}
};
}
}
=============== อัปเดต 2#
ฉันพลาดจุดที่มันควรจะเป็นแบบที่ไม่ปิดกั้น โฟลว์ปฏิกิริยาค่อนข้างคล้ายกับตัวอย่างที่ให้ไว้ในการอัปเดตครั้งแรกด้านบน การกำหนดค่าต่อไปนี้จะช่วยคุณได้:
@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);
}
ฉันสร้างตัวอย่างการใช้งานตามอินพุตข้างต้น ดูรายละเอียดที่นี่: GitHub - ความปลอดภัยของสปริงพร้อมการตรวจสอบชื่อทั่วไป
person
Hakan54
schedule
30.09.2020