四、用JSON作前后端分离的交互

在Spring Reactive Security中,Security过滤器是通过类ServerHttpSecurity配置的,用户认证过滤器是AuthenticationWebFilter,相当于SpringSecurity中的UsernamePasswordAuthenticationFilter

 

AuthenticationWebFilter中,用户名和密码的解析是通过 ServerAuthenticationConverter实现的。但是在ServerHttpSecurity的内部类表单配置类FormLoginSpec没有ServerAuthenticationConverter的配置属性:

	private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
			"/");

	private RedirectServerAuthenticationEntryPoint defaultEntryPoint;

	private ReactiveAuthenticationManager authenticationManager;

	private ServerSecurityContextRepository securityContextRepository;

	private ServerAuthenticationEntryPoint authenticationEntryPoint;

	private boolean isEntryPointExplicit;

	private ServerWebExchangeMatcher requiresAuthenticationMatcher;

	private ServerAuthenticationFailureHandler authenticationFailureHandler;

	private ServerAuthenticationSuccessHandler authenticationSuccessHandler = this.defaultSuccessHandler;

	private FormLoginSpec() {
	}

同时在ServerHttpSecurity没有替换过滤器的方法,只有添加过滤器的方法。所以要想解析JSON参数,必须要自定义AuthenticationWebFilter。

 

修改配置类MyReactiveSecurityConfig:

@Configuration
@Slf4j
public class MyReactiveSecurityConfig {
    @Autowired
    private ReactiveUserDetailsService reactiveUserDetailsService;

    @Autowired(required = false)
    private ReactiveUserDetailsPasswordService userDetailsPasswordService;


    private final static String LOGIN_PAGE="/doLogin";

//    @Bean
//    public ReactiveUserDetailsService reactiveUserDetailsService() {
//        UserDetails user = User.withUsername("user")
//                .password("12345")
//                .roles("USER")
//                .build();
//        return new MapReactiveUserDetailsService(user);
//    }

//    @Bean
//    public PasswordEncoder passwordEncoder() {
//        return NoOpPasswordEncoder.getInstance();
//    }

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

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(LOGIN_PAGE).permitAll()
                        .anyExchange().authenticated()
                )
                .httpBasic().disable()
                .formLogin().disable()
                .csrf().disable();

        http.addFilterAt(authenticationManager(), SecurityWebFiltersOrder.FORM_LOGIN);
        return http.build();
    }


    @Bean
    public ReactiveAuthenticationManager userDetailsRepositoryReactiveAuthenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager manager = new UserDetailsRepositoryReactiveAuthenticationManager(this.reactiveUserDetailsService);
        manager.setPasswordEncoder(passwordEncoder());
        manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
        return manager;
    }

    @Bean
    public AuthenticationWebFilter authenticationManager() {
        AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(userDetailsRepositoryReactiveAuthenticationManager());
        authenticationFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, LOGIN_PAGE));
        authenticationFilter.setAuthenticationFailureHandler((webFilterExchange, exception) -> {
            log.info("认证异常",exception);
            ServerWebExchange exchange = webFilterExchange.getExchange();

            exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);

            Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证失败".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);

            return exchange.getResponse().writeAndFlushWith(t -> {
                  t.onSubscribe(new Subscription() {
                    @Override
                    public void request(long l) {
                        
                    }

                    @Override
                    public void cancel() {

                    }
                });
                t.onNext(dataBufferFlux);
                t.onComplete();
            });
        });
        authenticationFilter.setAuthenticationConverter(new JsonServerAuthenticationConverter());
        authenticationFilter.setAuthenticationSuccessHandler((webFilterExchange, authentication) -> {
            log.info("认证成功:{}",authentication);
            ServerWebExchange exchange = webFilterExchange.getExchange();

            exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);

            Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证成功".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);

            return exchange.getResponse().writeAndFlushWith(t -> {
                  t.onSubscribe(new Subscription() {
                    @Override
                    public void request(long l) {
                        
                    }

                    @Override
                    public void cancel() {

                    }
                });
                t.onNext(dataBufferFlux);
                t.onComplete();
            });
        });
        authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
        return authenticationFilter;
    }
}

配置SecurityWebFilterChain时将httpBasic,formLogin,csrf全部禁止了。禁用csrf是为了演示方便。禁用httpBasic,formLogin这两个是为了只能用JSON格式参数才能登陆。同时要自定义AuthenticationWebFilter。增加的ReactiveAuthenticationManager是为了配置AuthenticationWebFilter的authenticationManagerResolver属性。同时重定义JsonServerAuthenticationConverter解析JSON参数。setAuthenticationFailureHandler处理认证失败的场景。setAuthenticationSuccessHandler处理认证成功的场景。都是返回JSON数据。

 

@Slf4j
public class JsonServerAuthenticationConverter extends ServerFormLoginAuthenticationConverter {

    private ObjectMapper objectMapper = new ObjectMapper();

    private String usernameParameter = "username";

    private String passwordParameter = "password";


    public JsonServerAuthenticationConverter() {

    }

    public JsonServerAuthenticationConverter(String usernameParameter,String passwordParameter) {
        this.usernameParameter = usernameParameter;
        this.passwordParameter = passwordParameter;
    }

    public Mono<Authentication> apply(ServerWebExchange exchange) {
        Flux<DataBuffer> body = exchange.getRequest().getBody();

        return body.map(t -> {
            String s = t.toString(StandardCharsets.UTF_8);
            log.info("参数:{}",s);
            Map<String , Object> map = null;
            try {
                map = objectMapper.readValue(s, Map.class);
            } catch (JsonProcessingException e) {
               log.info("解析JSON参数异常",e);
               map = null;
            }

            if (map != null && !map.isEmpty()) {
               String username = (String) map.get(usernameParameter);
               String password = (String) map.get(passwordParameter);

                Authentication unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
                return unauthenticated;
            }

            return null;
        }).next();
    }
}

从请求中获取JSON数据,解析成Map,根据用户名参数和密码参数构造Authentication。最后的next是为了将Flux转成Mono。因为返回值是Mono类型。ServerFormLoginAuthenticationConverter是org.springframework.security.web.server.ServerFormLoginAuthenticationConverter。本来要实现接口org.springframework.security.web.server.authentication.ServerAuthenticationConverter,偷下懒,就继承了org.springframework.security.web.server.ServerFormLoginAuthenticationConverter

Authentication unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password);

这个是必须的。不然在我的IDEA上一直提示类型不同。

 

用Postman演示:
 

 

在访问其他接口:
 

 

在演示认证失败:
 

 

还可以试下用浏览器是否可以登录。

posted @ 2023-06-06 21:21  shigp1  阅读(123)  评论(0编辑  收藏  举报