Security Reactive อย่างลึกด้วย Spring Security WebFlux

Sharing is caring!

ภาพหน้าปก: เสริมเกราะความปลอดภัยให้ระบบ Reactive ด้วย Spring Security WebFlux

Security Reactive อย่างลึกด้วย Spring Security WebFlux

เวลาอ่าน ≈ 20 นาที — คู่มือเชิงลึกสำหรับตั้งค่าความปลอดภัย Reactive บน Spring Security WebFlux ครบทุกเรื่องตั้งแต่ Authentication-WebFilter, SecurityWebFilterChain, JWT, OAuth2 Login, ไปจนถึง Reactive AuthorizationManager และการทดสอบ


สารบัญ

  1. Reactive Security ทำไมถึงต่าง?
  2. โครงสร้าง SecurityWebFilterChain
  3. AuthenticationWebFilter & ReactiveAuthenticationManager
  4. JWT กับ Reactive Authentication
  5. Reactive OAuth2 Login Flow
  6. Reactive AuthorizationManager
  7. บริหาร ReactiveSecurityContextHolder
  8. CSRF & CORS ใน WebFlux
  9. การทดสอบ Security ด้วย WebTestClient
  10. Best Practice & Checklist

1. Reactive Security ทำไมถึงต่าง?

บน Stack Servlet (Spring MVC) Spring Security ทำงานผ่าน javax.servlet.Filter หลายตัว. แต่ WebFlux รันบน Netty event-loop และใช้ org.springframework.web.server.WebFilter ที่เป็น non-blocking. ทุกส่วนจึงต้องคืนค่า Mono/Flux เพื่อหลีกเลี่ยงการบล็อก Thread

2. โครงสร้าง SecurityWebFilterChain

ภาพที่ 1 : Exchange → SecurityWebFilterChain → AuthenticationWebFilter → AuthorizationManager
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    return http
        .csrf(csrf -> csrf.disable())
        .authorizeExchange(ex -> ex
            .pathMatchers("/public/**").permitAll()
            .anyExchange().authenticated())
        .authenticationManager(reactiveAuthManager())
        .securityContextRepository(securityContextRepo())
        .build();
}
  • SecurityWebFilterChain – ประกอบ WebFilter ตาม config
  • AuthenticationWebFilter – รับ Credential สร้าง Authentication
  • AuthorizationWebFilter – ใช้ ReactiveAuthorizationManager ตรวจสิทธิ์

3. AuthenticationWebFilter & ReactiveAuthenticationManager

3.1 กำหนดวิธีอ่าน Credentials

@Bean
public AuthenticationWebFilter authenticationWebFilter() {
    AuthenticationWebFilter filter =
        new AuthenticationWebFilter(reactiveAuthManager());

    filter.setServerAuthenticationConverter(exchange -> {
        return Mono.zip(
                exchange.getRequest().getHeaders().getFirst("X-USER"),
                exchange.getRequest().getHeaders().getFirst("X-PASS"))
            .map(tuple -> new UsernamePasswordAuthenticationToken(tuple.getT1(),
                                                                  tuple.getT2()));
    });
    return filter;
}

3.2 ReactiveAuthenticationManager

@Bean
public ReactiveAuthenticationManager reactiveAuthManager(UserDetailsService svc,
                                                         PasswordEncoder pwd) {
    UserDetailsRepositoryReactiveAuthenticationManager mgr =
        new UserDetailsRepositoryReactiveAuthenticationManager(svc);
    mgr.setPasswordEncoder(pwd);
    return mgr;
}

4. JWT กับ Reactive Authentication

ภาพที่ 2 : Browser → Reactive App → Authentication Server → JWT Token

4.1 สร้าง JWT TokenFilter

public class JwtAuthWebFilter implements WebFilter {

  private final JwtDecoder decoder;

  public JwtAuthWebFilter(JwtDecoder decoder) { this.decoder = decoder; }

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
      return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
          .filter(h -> h.startsWith("Bearer "))
          .map(h -> h.substring(7))
          .flatMap(decoder::decode)
          .map(jwt -> new UsernamePasswordAuthenticationToken(
                           jwt.getSubject(), jwt, AuthorityUtils.NO_AUTHORITIES))
          .flatMap(auth -> chain.filter(exchange)
                                .contextWrite(withSecurityContext(auth)))
          .switchIfEmpty(chain.filter(exchange));
  }

  private Context withSecurityContext(Authentication auth) {
      return ReactiveSecurityContextHolder.withAuthentication(auth);
  }
}

4.2 ผูก Filter เข้า Chain

http.addFilterAt(new JwtAuthWebFilter(jwtDecoder), SecurityWebFiltersOrder.AUTHENTICATION);

5. Reactive OAuth2 Login Flow

ภาพที่ 3 : Reactive OAuth2 Login – User → Client → Authorization Server → Resource Server
@Bean
public SecurityWebFilterChain oauth2LoginChain(ServerHttpSecurity http) {
    return http
      .authorizeExchange(ex -> ex.anyExchange().authenticated())
      .oauth2Login(oauth2 -> oauth2.loginPage("/oauth2/authorization/github"))
      .oauth2Client(Customizer.withDefaults())
      .build();
}

6. Reactive AuthorizationManager

@Bean
public SecurityWebFilterChain chain(ServerHttpSecurity http) {
    return http
      .authorizeExchange(ex -> ex
          .pathMatchers(HttpMethod.POST, "/admin/**")
          .access((mono, ctx) -> mono
              .map(Authentication::getAuthorities)
              .flatMapMany(Flux::fromIterable)
              .any(a -> a.getAuthority().equals("ROLE_ADMIN"))
              .map(AuthorizationDecision::new))
          .anyExchange().permitAll())
      .build();
}

7. บริหาร ReactiveSecurityContextHolder

  • ใช้ .contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth))
  • ในการสลับ Scheduler (เช่น publishOn) ค่า Context จะไหลไปด้วย
  • เปิด DelegatingSecurityContextReactiveOperator ใน lib spring-security-core สำหรับงาน Batch

8. CSRF & CORS ใน WebFlux

http
  .csrf(csrf -> csrf
      .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
  .cors(cors -> cors.configurationSource(exchange -> {
        CorsConfiguration cfg = new CorsConfiguration();
        cfg.addAllowedOrigin("https://example.com");
        cfg.addAllowedMethod("*");
        cfg.setAllowCredentials(true);
        return cfg;
  }));

9. การทดสอบ Security ด้วย WebTestClient

@Import(SecurityConfig.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class JwtSecurityTest {

 @Autowired WebTestClient client;

 @Test
 void shouldRejectWhenNoToken() {
      client.get().uri("/protected")
            .exchange()
            .expectStatus().isUnauthorized();
 }

 @Test
 void shouldAllowWithValidToken() {
      String token = JwtUtil.generate("userA");
      client.get().uri("/protected")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
            .exchange()
            .expectStatus().isOk();
 }
}

10. Best Practice & Checklist

  1. ทุก WebFilter ต้อง non-blocking 100% (ตรวจ BlockHound)
  2. ใช้ ServerSecurityContextRepository เก็บ/เรียก Auth เพื่อลด latency
  3. ตั้งค่า spring.security.oauth2.client.provider.*.user-name-attribute ให้ตรง field
  4. เปิด Micrometer metric: spring.security prefix ติดตาม auth error rate
  5. ทดสอบ Load 2× ภาระงานจริง เพื่อจับ race condition ใน Reactive Context

สรุป

Spring Security WebFlux มอบเครื่องมือครบเครื่องสำหรับ Reactive Authentication, Authorization, และ Context Propagation. หากออกแบบ SecurityWebFilterChain, เลือก AuthenticationManager, และจัดการ ReactiveSecurityContextHolder อย่างถูกวิธี คุณจะได้ระบบที่ รวดเร็ว, ไม่บล็อก, และ ปลอดภัยระดับสากล รองรับทั้ง JWT, OAuth2, และ Custom Auth Provider ได้สบาย ๆ

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *