天天看點

Spring WebFlux Security結合R2DBC實作權限控制

環境:Springboot2.7.7

依賴管理

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
  <groupId>dev.miku</groupId>
  <artifactId>r2dbc-mysql</artifactId>
  <version>0.8.2.RELEASE</version>
</dependency>           

配置管理

spring:
  r2dbc:
    url: r2dbc:mysql://localhost:3306/testjpa?serverZoneId=GMT%2B8
    username: root
    password: 123123
    pool:
      initialSize: 100
      maxSize: 200
      maxCreateConnectionTime: 30s
---
logging:
  level:
    '[org.springframework.r2dbc]': DEBUG           

實體對象,Repository,Service

@Table("t_users")
public class Users implements UserDetails {
  @Id
  private Integer id ;
  private String username ;
  private String password ;
}           

這裡實體對象實作了UserDetials,在後續配置的ReactiveUserDetailsService 配置傳回值必須是UserDetails

Repository接口

public interface UsersRepository extends ReactiveSortingRepository<Users, String> {

  Mono<Users> findByUsername(String username) ;

}           

Service類

@Service
public class UsersService {

  @Resource
  private R2dbcEntityTemplate template;
  @Resource
  private UsersRepository ur ;

  public Mono<Users> queryUserByUsername(String username) {
    return ur.findByUsername(username) ;
  }
	
  public Mono<Users> queryUserByUsernameAndPassword(String username, String password) {
    return ur.findByUsernameAndPassword(username, password) ;
  }

  public Flux<Users> queryUsers() {
    return template.select(Users.class).all() ;
  }
	
}           

資料庫中插入幾條資料

Spring WebFlux Security結合R2DBC實作權限控制

單元測試

@SpringBootTest
class SpringBootWebfluxSecurity2ApplicationTests {

  @Resource
  private UsersService usersService ;
  
  @Test
  public void testQueryUserByUsername() throws Exception {
    usersService.queryUserByUsername("admin")
      .doOnNext(System.out::println)
      .subscribe() ;
      System.in.read() ;
  }

}           

輸出

2023-01-12 17:43:48.863 DEBUG 16612 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : ControllerAdvice beans: none
2023-01-12 17:43:48.896 DEBUG 16612 --- [           main] o.s.w.s.adapter.HttpWebHandlerAdapter    : enableLoggingRequestDetails='false': form data and headers will be masked to prevent unsafe logging of potentially sensitive data
2023-01-12 17:43:49.147  INFO 16612 --- [           main] ringBootWebfluxSecurity2ApplicationTests : Started SpringBootWebfluxSecurity2ApplicationTests in 1.778 seconds (JVM running for 2.356)
2023-01-12 17:43:50.141 DEBUG 16612 --- [actor-tcp-nio-2] o.s.r2dbc.core.DefaultDatabaseClient     : Executing SQL statement [SELECT t_users.id, t_users.username, t_users.password FROM t_users WHERE t_users.username = ?]
Users [id=3, username=admin, password=123123]           

正常,接下來就是進行Security的配置

@Configuration
@EnableReactiveMethodSecurity
public class ReactiveSecurityConfig {

  // 根據使用者名查詢使用者資訊
  @Bean
  public ReactiveUserDetailsService reativeUserDetailsService(UsersService usersService) {
    return username -> usersService.queryUserByUsername(username).map(user -> {
      return (UserDetails) user;
    });
  }
  // 根據查詢出的使用者進行密碼比對,沒有對密碼進行加密所有就進行簡單的equals比較
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new PasswordEncoder() {
      @Override
      public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
      }
      @Override
      public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.toString().equals(encodedPassword);
      }
    };
  }
  @Order(Ordered.HIGHEST_PRECEDENCE + 1)
  @Bean
  public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange((authorize) -> 
        authorize
          // 配置資源通路權限,對于靜态資源直接放行
          .pathMatchers("/resources/**", "/favicon.ico").permitAll()
          // /users開頭的接口必須擁有ROLE_ADMIN角色
          .pathMatchers("/users/**").hasRole("ADMIN")
          // /api開頭的接口通過自定義驗證邏輯控制                 
          .pathMatchers("/api/**").access((authentication, context) -> {
            return authentication.map(auth -> {
              if ("user1".equals(auth.getName())) {
                return new AuthorizationDecision(false);
              }
              MultiValueMap<String, String> params = context.getExchange().getRequest().getQueryParams();
              List<String> sk = params.get("sk");
              if (sk == null || sk.get(0).equals("u")) {
                return new AuthorizationDecision(false);
              }
              return new AuthorizationDecision(true);
            });
          }).anyExchange().authenticated());
    // 異常處理
    http.exceptionHandling(execSpec -> {
      // 無權限時響應政策
      execSpec.accessDeniedHandler((exchange, denied) -> {
        ServerHttpResponse response = exchange.getResponse() ;
        response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
        DataBuffer body = response.bufferFactory().allocateBuffer() ;
        body.write("無權限 - " + denied.getMessage(), StandardCharsets.UTF_8) ;
        return response.writeWith(Mono.just(body)) ;
      }) ;
      // 沒有登入配置統一認證入口,這裡預設系統提供的登入頁面
      execSpec.authenticationEntryPoint((exchange, ex) -> {
        ServerHttpResponse response = exchange.getResponse() ;
        response.getHeaders().add("Content-Type", "text/html;charset=UTF-8");
        DataBuffer body = response.bufferFactory().allocateBuffer() ;
        body.write("請先登入 - " + ex.getMessage(), StandardCharsets.UTF_8) ;
        return response.writeWith(Mono.just(body)) ;
      }) ;
    }) ;
    http.csrf(csrf -> csrf.disable());
    http.formLogin();
    return http.build();
  }

}           

以上就是Security的配置。這裡就不做測試了

原理

這裡對實作的原理做簡單的接收,主要核心是WebFilter

自動配置類中

public class ReactiveSecurityAutoConfiguration {
  // 這裡通過注解啟用了Security功能
  @EnableWebFluxSecurity
  static class EnableWebFluxSecurityConfiguration {
  }
}           

EnableWebFluxSecurity

@Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class,
		ReactiveOAuth2ClientImportSelector.class })
@Configuration
public @interface EnableWebFluxSecurity {
}           

這裡有個重要的ServerHttpSecurityConfiguration和WebFluxSecurityConfiguration配置類

@Configuration(proxyBeanMethods = false)
class ServerHttpSecurityConfiguration {
  // 該類用來自定義配置我們的資訊,也就是上面我們自定義的SecurityWebFilterChain
  // 通過ServerHttpSecurity建構SecurityWebFilterChain
  @Bean(HTTPSECURITY_BEAN_NAME)
  @Scope("prototype")
  ServerHttpSecurity httpSecurity() {
    ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
    return http.authenticationManager(authenticationManager())
      .headers().and()
      .logout().and();
	}
}           

WebFluxSecurityConfiguration

配置類最主要就是用來配置一個WebFilter

@Configuration(proxyBeanMethods = false)
class WebFluxSecurityConfiguration {
  private List<SecurityWebFilterChain> securityWebFilterChains;
  // 注入目前系統中的所有SecurityWebFilterChain,主要就是我們自定義實作的該類型
  @Autowired(required = false)
  void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) {
    this.securityWebFilterChains = securityWebFilterChains;
  }
  @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
  @Order(WEB_FILTER_CHAIN_FILTER_ORDER)
  WebFilterChainProxy springSecurityWebFilterChainFilter() {
    return new WebFilterChainProxy(getSecurityWebFilterChains());
  }
  private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
    List<SecurityWebFilterChain> result = this.securityWebFilterChains;
    if (ObjectUtils.isEmpty(result)) {
      return Arrays.asList(springSecurityFilterChain());
    }
    return result;
  }
}           

WebFilterChainProxy核心過濾器

public class WebFilterChainProxy implements WebFilter {

  private final List<SecurityWebFilterChain> filters;
  @Override
  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    return Flux.fromIterable(this.filters)
      .filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange)).next()
      .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
      .flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
      .map((filters) -> new FilteringWebHandler(chain::filter, filters)).map(DefaultWebFilterChain::new)
      .flatMap((securedChain) -> securedChain.filter(exchange));
  }

}           

以上就是WebFlux中應用Security的原理

完畢!!!

關注我長期更新

Spring WebFlux Security結合R2DBC實作權限控制
Spring WebFlux Security結合R2DBC實作權限控制