Spring Boot 2.x实战91 - 响应式编程6 - 响应式安全控制(Reactive Spring Security)

5. Reactive Spring Security

5.1 Reactive Spring Security原理

Spring MVC的Security是通过Servlet的Filter实现的,而WebFlux的响应式Security是基于WebFilter实现的,由一些列的WebFilter形成的过滤器链。

  • 认证

    Spring WebFlux下的响应式安全和Spring MVC下的安全认证机制也是有概念对应的:

    Spring WebFlux Security Spring Web MVC Security
    AuthenticationWebFilter UsernamePasswordAuthenticationFilter
    ReactiveAuthenticationManager AuthenticationManager
    UserDetailsRepositoryReactiveAuthenticationManager ProviderManager
    AuthenticationManager
    ReactiveUserDetailsService UserDetailsService
  • 授权

    授权也一样,概念上也是对应的:

    Spring WebFlux Security Spring WebFlux Security
    AuthorizationWebFilter FilterSecurityInterceptor
    ReactiveAuthorizationManager AccessDecisionManager
  • 其它对等的还有:

    Spring WebFlux Security Spring WebFlux Security
    @EnableWebFluxSecurity @EnableWebSecurity
    @EnableReactiveMethodSecurity @EnableGlobalMethodSecurity
    ReactiveSecurityContextHolder SecurityContextHolder

5.2 Spring Boot的自动配置

Spring Boot为响应式安全提供了下面两个配置:

  • ReactiveSecurityAutoConfiguration:使用@EnableWebFluxSecurity开启Spring WebFlux安全的支持;
  • ReactiveUserDetailsServiceAutoConfiguration:配置一个reactiveUserDetailsService的Bean。

下面我们将用一个例子来学习响应式的安全机制,它和我们前面学的Spring MVC下的安全用法是很类似的。

5.3 示例

5.1.3.1 新建应用

新建应用,信息如下:

Group:top.wisely

Artifact:learning-reactive-security

Dependencies:Spring SecuritySpring Reactive WebSpring Data Reactive MongoDBLombok

build.gradle文件中的依赖如下:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	//...
}

Spring WebFlux Security和Spring MVC Security都在Spring Security中。

5.1.3.2 连接MongoDB

application.yml中配置连接MongoDB

spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      username: wisely
      password: zzzzzz
      database: first_db
5.1.3.3 定义领域模型
  • 安全相关模型

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Document(collection = "user")
    public class SysUser implements UserDetails {
        @Id
        private String id;
    
        private String username;
    
        private String password;
    
        private Set<SysAuthority> sysAuthorities;
    
        private Boolean enable = true;
    
        public SysUser(String username, String password, Set<SysAuthority> sysAuthorities) {
            this.username = username;
            this.password = password;
            this.sysAuthorities = sysAuthorities;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.sysAuthorities
                    .stream()
                    .map(authority -> new SimpleGrantedAuthority(authority.getName()))
                    .collect(Collectors.toCollection(HashSet::new));
        }
      //省略其它UserDetails接口方法
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class SysAuthority {
        private String name;
    }
    
  • 业务相关模型

  • @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Document(collection = "people")
    public class Person {
        @Id
        private String id;
        private String name;
        private Integer age;
    
        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    }
    
5.1.3.4 Repository
public interface SysUserRepository extends ReactiveMongoRepository<SysUser, String> {
    Mono<UserDetails> findByUsername(String username);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
}
5.1.3.5 获取用户

我们使用ReactiveUserDetailsService实现类来获取用户:

public class CustomReactiveUserDetailsService implements ReactiveUserDetailsService {

    @Autowired
    SysUserRepository sysUserRepository;

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        return sysUserRepository.findByUsername(username)
                .switchIfEmpty(Mono.error(new UsernameNotFoundException("User Not Found")))
                .map(UserDetails.class::cast);
    }
}
5.1.3.6 安全配置
@Configuration
@EnableReactiveMethodSecurity//1
public class ReactiveSecurityConfig {

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){//2
        return http.authorizeExchange()
                .anyExchange().authenticated() //3
             .and()
                .csrf().disable() //4
                .httpBasic() //5
             .and()
                .build();
    }

    @Bean
    CustomReactiveUserDetailsService reactiveUserDetailsService(){ //6
        return new CustomReactiveUserDetailsService();
    }

    @Bean
    PasswordEncoder passwordEncoder(){ //7
        return new BCryptPasswordEncoder();
    }
}
  1. 使用@EnableReactiveMethodSecurity开启方法安全的支持;
  2. 定义SecurityWebFilterChain对安全进行控制,使用ServerHttpSecurity构造过滤器链;
  3. 所有请求都需要通过认证;
  4. 关闭CSRF(Cross-site request forgery)跨站请求伪造,本例的客户端是自己;
  5. 使用HTTP Basic方式登录;
  6. 注册CusotmReactiveUserDetailsService
  7. 注册PasswordEncoder
5.1.3.7 控制器
@RestController
public class UserController {
    @GetMapping("/user") //可使用@AuthenticationPrincipal获得用户信息,登录即可
    public Mono<String> user(@AuthenticationPrincipal Mono<SysUser> userMono){
        return userMono.map(user -> user.getUsername());
    }
}
@RestController
@RequestMapping("/people")
public class PersonController {

    PersonRepository personRepository;

    public PersonController(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }
    @PostMapping
    @PreAuthorize("hasAuthority('personAdd')") //使用方法安全,定义控制器权限为personAdd
    public Mono<Person> add(@RequestBody Person person){
        return personRepository.save(person);
    }

    @GetMapping
    @PreAuthorize("hasAuthority('personList')")//使用方法安全,定义控制器权限为personList
    public Flux<Person> list(){
        return personRepository.findAll();
    }
}
5.1.3.8 WebClient
@Component
public class ControllerClient {

    WebClient webClient;

    public ControllerClient(WebClient.Builder builder) {
        this.webClient = builder.build();
    }

    public void addPersonByUser(){
        System.out.println("普通用户添加Peron,不具备权限");
        Mono<Person> mono = webClient
                .post()
                .uri("http://localhost:8080/people")
                .header(HttpHeaders.AUTHORIZATION,
                        "Basic " + Base64Utils.encodeToString("wyf:111111".getBytes(Charset.defaultCharset())))
                .body(Mono.just(new Person("aaa", 33)), Person.class)
                .retrieve()
                .bodyToMono(Person.class);
        mono.subscribe(System.out::println);
    }

    public void addPersonByAdmin(){
        System.out.println("管理员用户添加Peron,具备权限");
					//...
            .body(Mono.just(new Person("bbb", 34)), Person.class)
            .header(HttpHeaders.AUTHORIZATION,
                    "Basic " + Base64Utils.encodeToString("admin:admin".getBytes(Charset.defaultCharset())))
					//...
    }

    public void listPersonByUser(){
        System.out.println("普通用户查看Person列表,具备权限");
        Flux<Person> flux = webClient
                .get()
                .uri("http://localhost:8080/people")
                .header(HttpHeaders.AUTHORIZATION,
                        "Basic " + Base64Utils.encodeToString("wyf:111111".getBytes(Charset.defaultCharset())))
                .retrieve()
                .bodyToFlux(Person.class);
        flux.subscribe(System.out::println);
    }

    public void listPersonByAdmin(){
        System.out.println("管理员用户查看Person列表,具备权限");
							//...
                .header(HttpHeaders.AUTHORIZATION,
                        "Basic " + Base64Utils.encodeToString("admin:admin".getBytes(Charset.defaultCharset())))
						//...
    }

    public void getUserInfo(){
        System.out.println("普通用户查看用户信息,登录即可");
        Mono<String> mono = webClient
                .get()
                .uri("http://localhost:8080/user")
                .header(HttpHeaders.AUTHORIZATION,
                        "Basic " + Base64Utils.encodeToString("wyf:111111".getBytes(Charset.defaultCharset())))
                .retrieve()
                .bodyToMono(String.class);
        mono.subscribe(System.out::println);
    }

    public void getAdminInfo(){
        System.out.println("管理员用户查看用户信息,登录即可");
							//...
                .header(HttpHeaders.AUTHORIZATION,
                        "Basic " + Base64Utils.encodeToString("admin:admin".getBytes(Charset.defaultCharset())))
						//...
    }
}

当前值得注意的是用header()方法进行HTTP Basic登录:

.header(HttpHeaders.AUTHORIZATION,
                    "Basic " + Base64Utils.encodeToString("wyf:111111".getBytes(Charset.defaultCharset())))
5.1.3.9 验证
  • 初始化用户

    @Bean
    CommandLineRunner initUsersClr(PasswordEncoder passwordEncoder,
                            SysUserRepository sysUserRepository){
       return args -> {
          SysUser user = new SysUser("wyf",
                passwordEncoder.encode("111111"),
                Stream.of(new SysAuthority("personList")).collect(Collectors.toSet()));
          SysUser admin = new SysUser("admin",
                passwordEncoder.encode("admin"),
                Stream.of(new SysAuthority("personList"), new SysAuthority("personAdd"))
                      .collect(Collectors.toSet()));
          sysUserRepository.save(user).subscribe();
          sysUserRepository.save(admin).subscribe();
       };
    }
    

    wyf用户具备personList权限;admin用户具备personListpersonAdd权限。

  • 调用接口

  • @Bean
    CommandLineRunner webClientClr(ControllerClient controllerClient){
       return args -> {
          controllerClient.addPersonByUser();
          Thread.sleep(1000);
          controllerClient.addPersonByAdmin();
          Thread.sleep(1000);
          controllerClient.listPersonByUser();
          Thread.sleep(1000);
          controllerClient.listPersonByAdmin();
          Thread.sleep(1000);
          controllerClient.getUserInfo();
          Thread.sleep(1000);
          controllerClient.getAdminInfo();
       };
    }
    
  • 输出结果
    Spring Boot 2.x实战91 - 响应式编程6 - 响应式安全控制(Reactive Spring Security)_第1张图片

新书推荐:

我的新书《从企业级开发到云原生微服务:Spring Boot 实战》已出版,内容涵盖了丰富Spring Boot开发的相关知识
购买地址:https://item.jd.com/12760084.html
在这里插入图片描述

主要包含目录有:

第一章 初识Spring Boot(快速领略Spring Boot的美丽)
第二章 开发必备工具(对常用开发工具进行介绍:包含IntelliJ IDEA、Gradle、Lombok、Docker等)
第三章 函数式编程
第四章 Spring 5.x基础(以Spring 5.2.x为基础)
第五章 深入Spring Boot(以Spring Boot 2.2.x为基础)
第六章 Spring Web MVC
第七章 数据访问(包含Spring Data JPA、Spring Data Elasticsearch和数据缓存)
第八章 安全控制(包含Spring Security和OAuth2)
第九章 响应式编程(包含Project Reactor、Spring WebFlux、Reactive NoSQL、R2DBC、Reactive Spring Security)
第十章 事件驱动(包含JMS、RabbitMQ、Kafka、Websocket、RSocket)
第11章 系统集成和批处理(包含Spring Integration和Spring Batch)
第12章 Spring Cloud与微服务
第13章 Kubernetes与微服务(包含Kubernetes、Helm、Jenkins、Istio)
多谢大家支持。

你可能感兴趣的:(Spring,Boot2.x实战全集,Spring,Boot2.x实战,-,响应式编程,spring,boot,reactive,spring,security)