Security Reactive


在〈簡介 WebFlux〉中,曾經引用 Spring 官方網站的技術堆疊示意圖,其中看到在 Reactive Stack 中,Spring Security 提供了 Reactive 的版本,想在 WebFlux 使用 Security Reactive 的話,除了使用 Reactive Web 的 Starter 之外,還要加上 Security 的 Starter。

預設只要有 Security 的話,就會啟用頁面防護,與〈Security 設置〉中談到的一樣,預設的使用者名稱為 user,密碼為隨機產生,可以自行設置在 application.properties 中設定使用者名稱與密碼。

如果要設定特定頁面防護,可以透過 ServerHttpSecurity,這是個類似 HttpSecurity 的物件,你可以透過它設定防護條件,然後建立一個 SecurityWebFilterChain

@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
    return http
             .authorizeExchange()
                 .pathMatchers("/member.html").hasRole("MEMBER")
                 .pathMatchers("/admin.html").hasRole("ADMIN")
             .anyExchange().permitAll()
             .and()
             .formLogin()
             .and().build();
}

在上面的設定中,請求 member.html、admin.html 會需要驗證,角色各必須是 MEMBERADMIN,其他頁面則不需要防護,從 SecurityWebFilterChain 可以看到,Reactive 版本的 Security 基本上也是由過濾器來處理,不過並不是 Servlet API 的 Filter,而是 Spring 本身的 org.springframework.web.server.WebFilter

若要設置使用者、密碼、角色資訊呢?在〈UserDetailsService〉談過,可以自訂 UserDetailsService 來自訂如何取得 UserDetails,以進一步供做驗證時使用,在 Reactive 版本的 Security 中,則是透過實作 ReactiveUserDetailsService 來處理。例如:

@Bean
public ReactiveUserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
    UserDetails admin = User
          .withUsername("admin")
          .password(passwordEncoder.encode("admin12345678"))
          .roles("ADMIN", "MEMBER")
          .build();

    UserDetails caterpillar = User
          .withUsername("caterpillar")
          .password(passwordEncoder.encode("12345678"))
          .roles("MEMBER")
          .build();

    return new MapReactiveUserDetailsService(admin, caterpillar);
}

MapReactiveUserDetailsService 是個 ReactiveUserDetailsService 的實作類別,建構時可以指定多個 UserDetails 實例,在這邊借由它來實現一個記憶體中的驗證資料來源,為了提供 PasswordEncoder 實作,別忘了加上:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

當然,你也可以實現一個 ReactiveUserDetailsService,從資料庫中取得 UserDetails,資料庫在 Reactive 的支援上,之後的文件再來討論。

你也許會想起〈AuthenticationProvider〉,在 Reactive 這方面有對應的版本嗎?ServerHttpSecurity 有個 authenticationManager 方法,可以接受ReactiveAuthenticationManager 實例,它有個方法必須實作:

Mono<Authentication> authenticate(Authentication authentication)

也就是說,如果你有自己的驗證實作方式,可以自行定義 ReactiveAuthenticationManager,例如,純綷只是示範 API 的運用,故意將上頭的記憶體驗證來源,改用 ReactiveAuthenticationManager 實現的話會是:

@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
    return http
             .authenticationManager(auth -> {
                 String name = auth.getName();
                 String password = auth.getCredentials().toString();

                 if(name.equals("caterpillar") && password.equals("12345678")) {
                     return Mono.just(
                        new UsernamePasswordAuthenticationToken("caterpillar", "12345678", 
                            AuthorityUtils.createAuthorityList("ROLE_MEMBER")
                        )
                    );
                 }

                 if(name.equals("admin") && password.equals("admin12345678")) {
                     return Mono.just(
                        new UsernamePasswordAuthenticationToken("admin", "admin12345678", 
                            AuthorityUtils.createAuthorityList("ROLE_MEMBER", "ROLE_ADMIN")
                        )
                    );
                 }

                 return null;
             })
             .authorizeExchange()
                 .pathMatchers("/member.html").hasRole("MEMBER")
                 .pathMatchers("/admin.html").hasRole("ADMIN")
             .anyExchange().permitAll()
             .and()
             .formLogin()
             .and().build();
}

你可以在 SecurityReactive 找到以上的範例專案。