Spring Security 系列(3) —— Spring Security & Webflux

2022-09-08 11:52:52 浏览数 (1)

Spring Security & Webflux

文章目录

  • Spring Security & Webflux
    • Webflux Spring Security
      • 初始准备
        • 引入 POM
        • 修改配置文件
        • 编写主启动类
      • 开启表单登陆
        • 添加 Controller
        • 添加 WebSecurity 的配置类
        • 测试效果
    • Webflux Spring Security OAuth2
      • OAuth2 客户端
        • OAuth2 核心类
        • 密码模式实现
          • 修改 yml 配置文件
          • 修改 Webflux 的配置
          • 添加登陆用的 DTO
          • 添加 OAuth2 配置类
          • 添加 Controller
        • 授权码模式实现
          • 注入一个 client 用于获取授权码返回的 token 信息
          • 修改 Controller
      • OAuth2 资源服务器
        • 使用 OAuth2 资源服务器
          • 配置 yaml
          • 添加资源服务器配置
          • 修改 Controller 并测试效果
          • 最终测试效果

Webflux Spring Security

初始准备

引入 POM
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</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-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
修改配置文件

application.yml

代码语言:javascript复制
server:
  port: 8089
spring:
  main:
    allow-bean-definition-overriding: true
编写主启动类
代码语言:javascript复制
@EnableWebFlux
@SpringBootApplication
public class SpringSecurityOAuth2TestApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringSecurityOAuth2TestApplication.class);
        application.setWebApplicationType(WebApplicationType.REACTIVE);
        application.run(args);
    }
}

开启表单登陆

表单验证登陆时 Serverlet 与 Webflux 的相关核心类的对照情况

添加 Controller
代码语言:javascript复制
@RestController
public class LoginController {

    @GetMapping("/") // 默认登陆成功后跳转
    public Mono<String> main(){
        return Mono.just("main");
    }

    @GetMapping("/test1")
    public Mono<String> test1(){
        return Mono.just("test1");
    }

    @GetMapping("/test2")
    public Mono<String> test2(){
        return Mono.just("test2");
    }

    @GetMapping("/test3")
    public Mono<Integer> test3(){
        return Mono.fromSupplier(() -> new Random().nextInt());
    }

    @GetMapping("/test4")
    public Mono<Double> test4(){
        return Mono.fromSupplier(() -> new Random().nextDouble());
    }

}
添加 WebSecurity 的配置类
代码语言:javascript复制
@Configuration
@EnableWebFluxSecurity
public class WebfluxConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
        http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
                .pathMatchers("/test1").permitAll()
                .pathMatchers("/test2").hasAnyRole("root")
                .pathMatchers("/test3").hasAnyRole("admin")
                .pathMatchers("/test4").hasAnyAuthority("root","user")
                .anyExchange().authenticated()
        );
        http.formLogin(Customizer.withDefaults());// 开启表单验证
        http.httpBasic(Customizer.withDefaults());// 开启 Basic 验证


        http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
        http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
            exchange -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Collections.singletonList("*"));
                config.setAllowedHeaders(Collections.singletonList("*"));
                config.setAllowedMethods(Collections.singletonList("*"));
                config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
                config.setAllowCredentials(true);
                config.applyPermitDefaultValues();
                return config;
            }
        ));

        return http.build();
    }

    @Bean // 密码加密器曝露,其也会被自动注入到 webflux security 中
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean // 开启表单验证时一定要曝露一个 ReactiveUserDetailService,他会被自动注入到 WebfluxSecurity 中
    public MapReactiveUserDetailsService userDetailsService(){
        PasswordEncoder passwordEncoder = passwordEncoder();
        UserDetails root = User.withUsername("root") // 创建两个 UserDetail
                .password(passwordEncoder.encode("123456"))
//                .authorities("root","ROLE_user","user")
                .roles("root","user") // role 与 authorities 会相互覆盖只能用一个
                .build();
        UserDetails user = User.withUsername("usr")
                .password(passwordEncoder.encode("password"))
                .authorities("ROLE_user","user")
                .build();
        UserDetails role = User.withUsername("test")
                .password(passwordEncoder.encode("admin"))
                .authorities("ROLE_admin") // role 为 admin 的权限就是 ROLE_admin
                .build();
        return new MapReactiveUserDetailsService(root,user,role);
        // 注意: MapReactiveUserDetailsService 在此段代码中只是用于模拟自我实现的 ReactiveUserDetailService
        //       在实际开发中可以自需要自己实现这个接口
    }
}
测试效果

进入登陆页面,输入 test 的用户名和密码,在登陆成功后请求 test3 可以看到被校验通过

Webflux Spring Security OAuth2

OAuth2 客户端

OAuth2 核心类

WebFlux 与 Servelet 的 OAuth2 核心类对照表

WebFlux

Servelet

ClientRegistration

ClientRegistration

ReactiveClientRegistrationRepository

ClientRegistrationRepository

OAuth2AuthorizedClient

ClientRegistrationOAuth2AuthorizedClient

ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService

ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider

OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider

密码模式实现
修改 yml 配置文件

application.yml

代码语言:javascript复制
auth_server: http://localhost:8088/ # 指定授权服务器地址
spring:
  main:
    allow-bean-definition-overriding: true
  security:
    oauth2:
      client:
        registration:
          test: # registrationId
            clientId: client # clientId
            clientSecret: yourSecret # clientSecret
            authorizationGrantType:  password # authorization_code # 授权类型
            scope: all # 授权范围
        provider:
          test: # providerId
            authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
            tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
修改 Webflux 的配置

代码语言:javascript复制
@Configuration
@EnableWebFluxSecurity
public class WebfluxConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
        http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
                .pathMatchers("/oauth/**").permitAll()
                .anyExchange().authenticated()
        );

        http.oauth2Client(Customizer.withDefaults());// 使用 OAuth2 Client

        http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
        http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
            exchange -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Collections.singletonList("*"));
                config.setAllowedHeaders(Collections.singletonList("*"));
                config.setAllowedMethods(Collections.singletonList("*"));
                config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
                config.setAllowCredentials(true);
                config.applyPermitDefaultValues();
                return config;
            }
        ));

        return http.build();
    }

}
添加登陆用的 DTO
代码语言:javascript复制
@Data
public class UserDto {
    private String username;
    private String password;
}
添加 OAuth2 配置类

OAuth2Configuration

代码语言:javascript复制
@Configuration
public class OAuth2Configuration {
    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .password()
                        .refreshToken()
                        .build();

        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters,
        // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

        return authorizedClientManager;
    }

    private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
        return authorizeRequest -> {
            Map<String, Object> contextAttributes = Collections.emptyMap();
            UserDto exchange = authorizeRequest.getAttribute(UserDto.class.getName());
            if (StringUtils.hasText(exchange.getUsername()) && StringUtils.hasText(exchange.getPassword())) {
                contextAttributes = new HashMap<>();
                // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, exchange.getUsername());
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, exchange.getPassword());
            }
            return Mono.just(contextAttributes);
        };
    }
添加 Controller

Oauth2Controller

代码语言:javascript复制
@RestController
public class OAuth2Controller {

    @Autowired
    ReactiveOAuth2AuthorizedClientManager clientManager;

    @PostMapping("/oauth/login")
    public Mono<String> login(@RequestBody UserDto user){
        Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
                .principal(authentication)
                .attribute(UserDto.class.getName(),user)
                .build();

        return clientManager.authorize(authorizeRequest).map(oAuth2AuthorizedClient -> {
           OAuth2AccessToken accessToken =  oAuth2AuthorizedClient.getAccessToken();
           if(accessToken != null && StringUtils.hasLength(accessToken.getTokenValue())){
               System.out.println(accessToken.getTokenValue());
               return accessToken.getTokenValue();
           }
           return "no info";

        });
    }
}
授权码模式实现
注入一个 client 用于获取授权码返回的 token 信息

OAuth2Configuration

代码语言:javascript复制
@Bean
public WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient(){    
    return new WebClientReactiveAuthorizationCodeTokenResponseClient();
}
修改 Controller

Oauth2Controller

代码语言:javascript复制
@Autowired
WebClientReactiveAuthorizationCodeTokenResponseClient client;

@Autowired
ReactiveClientRegistrationRepository clientRegistrationRepository;

@GetMapping("/oauth/loginWithCode")
public Mono<String> test(@PathParam("code")String code){
    if(StringUtils.hasLength(code)){
        OAuth2AuthorizationRequest oAuth2AuthorizeRequest = OAuth2AuthorizationRequest.authorizationCode()
                .authorizationUri("http://localhost:8088/oauth/authorize")
                .clientId("client")
                .build();
        OAuth2AuthorizationResponse response = OAuth2AuthorizationResponse.success(code).
                redirectUri("http://www.baidu.com").build();
        OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(oAuth2AuthorizeRequest,response);
        OAuth2AuthorizationCodeGrantRequest request = new OAuth2AuthorizationCodeGrantRequest(
                clientRegistrationRepository.findByRegistrationId("test").block() ,exchange);
        return client.getTokenResponse(request).map(res -> res.getAccessToken().getTokenValue());
    }
    return Mono.just("false");
}

OAuth2 资源服务器

使用 OAuth2 资源服务器
配置 yaml
代码语言:javascript复制
server:
  port: 8089
  
auth_server: http://localhost:8088/ # 指定授权服务器地址
spring:
  main:
    allow-bean-definition-overriding: true
  security:
    oauth2:
      client:
        registration:
          test: # registrationId
            clientId: client # clientId
            clientSecret: yourSecret # clientSecret
            redirectUri: http://localhost:${server.port}/test/2
            authorizationGrantType:  password # authorization_code # 授权类型
            scope: all # 授权范围
        provider:
          test: # providerId
            authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
            tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
      resourceserver:
        jwt:
          public-key-location: classpath:public.cert # 指定公钥位置
添加资源服务器配置

WebfluxConfiguration

代码语言:javascript复制
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
    http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
            .pathMatchers("/oauth/**").permitAll()
            .anyExchange().authenticated()
    );
    http.oauth2Client(Customizer.withDefaults());// 使用 OAuth2 Client
    // 对资源服务器进行相关配置
    http.oauth2ResourceServer(resource ->{
        resource.jwt(); // 开启资源服务器的 Jwt
    });
    http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
    http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
        exchange -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(Collections.singletonList("*"));
            config.setAllowedHeaders(Collections.singletonList("*"));
            config.setAllowedMethods(Collections.singletonList("*"));
            config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
            config.setAllowCredentials(true);
            config.applyPermitDefaultValues();
            return config;
        }
    ));
    return http.build();
}
修改 Controller 并测试效果
代码语言:javascript复制
@Autowired
public ReactiveJwtDecoder jwtDecoder;
@PostMapping("/oauth/login")
public Mono<String> login(@RequestBody UserDto user){
    Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
    OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
            .principal(authentication)
            .attribute(UserDto.class.getName(),user)
            .build();
    return clientManager.authorize(authorizeRequest).map(oAuth2AuthorizedClient -> {
       OAuth2AccessToken accessToken =  oAuth2AuthorizedClient.getAccessToken();
       if(accessToken != null && StringUtils.hasLength(accessToken.getTokenValue())){
           System.out.println(accessToken.getTokenValue());
           jwtDecoder.decode(accessToken.getTokenValue()).subscribe(jwt -> { // 解码
               System.out.println(jwt.getClaims()); // 打印信息
               System.out.println(jwt.getId()); // 打印 jwt
           });
           return accessToken.getTokenValue();
       }
       return "noinfo";
    });
}
最终测试效果

0 人点赞