Фактический WebFlux Spring Cloud Gateway анализирует тело запроса и выдает указанный код ошибки и информацию

Обзор

При разработке микросервисов на основе Spring Cloud используйте в качестве шлюза собственный шлюз Spring Cloud, и все запросы необходимо перенаправлять через службу шлюза.

Чтобы предотвратить очистку данных вредоносными запросами, бизнес-запросы необходимо перехватывать, поэтому в службу шлюза можно добавить фильтр перехвата. На основании этого имеется следующий исходный код:


@Slf4j
@Component
public class BlockListFilter extends AbstractGatewayFilterFactory {
    
    
	private static final String DIALOG_URI = "/dialog/nextQuestion";
    @Resource
    private AssessmentBlockListService assessmentBlockListService;
    @Lazy
    @Resource
    private RemoteUserService remoteUserService;
    @Lazy
    @Resource
    private RemoteRcService remoteRcService;
    @Lazy
    @Autowired
    private RemoteOAuthService remoteOAuthService;
    @Value("${blockListSwitch:true}")
    private Boolean blockListSwitch;

    @Override
    public GatewayFilter apply(Object config) {
    
    
        return (exchange, chain) -> {
    
    
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
            ServerHttpRequest serverHttpRequest = exchange.getRequest();
            String uri = serverHttpRequest.getURI().getPath();
            HttpHeaders httpHeaders = serverHttpRequest.getHeaders();
            // 只处理相关url
            if (blockListSwitch && StringUtils.equalsIgnoreCase(uri, DIALOG_URI)) {
    
    
                String token = httpHeaders.getFirst("Authorization");
                String pureToken = StringUtils.replaceIgnoreCase(token, "Bearer ", "");
                BaseUserInfo baseUserInfo = UserUtil.getBaseUserInfo(pureToken);
                if (baseUserInfo == null) {
    
    
                    log.warn("传入的token非法:{}", token);
                    return chain.filter(exchange);
                }
                // 从JWT token中解析用户信息(不含mobile等敏感信息)
                String channel = baseUserInfo.getChannel();
                String userKey = baseUserInfo.getUserkey();
                UserParam userParam = new UserParam();
                userParam.setKey(userKey);
                userParam.setChannel(channel);
                // Feign远程请求user服务获取mobile信息
                Response<UserAccountVO> userAccountResponse = remoteUserService.baseQuery(userParam);
                if (userAccountResponse.getCode() != 0) {
    
    
                    log.warn("未获取到userKey={}的用户信息!", userKey);
                    return chain.filter(exchange);
                }
                UserAccountVO userAccountVO = userAccountResponse.getData();
                log.info("blocklist filter user={}", JsonUtil.beanToJson(userAccountVO));
                String mobile = userAccountVO.getMobile();
                if (StringUtils.isNotBlank(userKey)) {
    
    
                	// 具体的拦截业务逻辑
                    this.process(uri, userKey, mobile, channel, pureToken);
                }
                return chain.filter(exchange);
            }
            return chain.filter(exchange);
        };
    }

	private String process(String uri, String userKey, String mobile, String channel, String token) {
    
    
        DetectUserDTO detectUserDTO = new DetectUserDTO();
        detectUserDTO.setChannel(channel);
        detectUserDTO.setMobile(mobile);
        detectUserDTO.setUserKey(userKey);
        detectUserDTO.setUri(uri);
        Response<DetectUserVO> detectUserVOResponse = remoteRcService.detectUser(detectUserDTO);
        if (detectUserVOResponse.getCode() != 0) {
    
    
            log.warn("mobile={} 风控接口返回异常:{}", mobile, JsonUtil.beanToJson(detectUserVOResponse));
            return null;
        }
        DetectUserVO detectUserVO = detectUserVOResponse.getData();
        if (detectUserVO.getIsInAllowList()) {
    
    
            log.info("在白名单中,放行");
        } else if (detectUserVO.getIsInBlockList()) {
    
    
            log.info("在黑名单中,拦截处理...");
            this.logout(token);
            return "当前手机号问诊次数已达今日上限!";
        }
        return null;
    }
    
    private void logout(String token) {
    
    
        // 强制下线,踢出登录态
        LogoutDto logoutDto = new LogoutDto();
        logoutDto.setToken(pureToken);
        remoteOAuthService.logout(logoutDto);
    }
}

Приведенный выше код реализует только: определяет URL-адрес запроса трафика, а затем определяет, запускает ли пользователь механизм черного списка контроля рисков. Если черный список срабатывает, состояние входа в систему выбрасывается, и пользователь вынужден выйти из системы, то есть пользователь не может использовать приложение.

нуждаться

Перехват исключений

Вышеупомянутый метод исключения состояния входа в систему слишком прост и груб. Приложение имеет несколько функциональных модулей. Некоторые URL-адреса запросов запускают механизм черного списка, вынуждая пользователя выйти из системы и не имея возможности использовать другие функции Приложения. Пользователям должно быть разрешено использовать другие функции модуля, помимо запуска модулей, занесенных в черный список. Конечно, у каждого свое мнение о том, стоит ли форсировать дизайн офлайн-взаимодействия.

Если подробнее описать ситуацию, с которой я столкнулся, то болевая точка заключается в том, что студенты фронтенда (так называемая концепция большого фронтенда, включая приложения для iOS и Android) напрямую возвращают единую страницу ошибки в ответ на эту ситуацию: [Извините, пожалуйста вернитесь и попробуйте еще раз! ], и на этой странице нет никакого дизайна пользовательского интерфейса, только упомянутая выше копия приглашения. Когда пользователи увидят эту копию, они снова войдут в систему (поскольку бэкэнд реализовал логику исключения состояния входа).После того, как приложение успешно выполнено, оно проверяется шлюзом, и обнаруживается, что пользователь все еще запускает черный список. , а затем снова вылетает из состояния входа в систему, попадая в бесконечный цикл.

На основании этого внесены следующие изменения: не выкидываясь из состояния входа в систему, можно продолжать использовать другие функции модуля.При использовании модуля, запускающего механизм черного списка, бэкенд возвращает код ошибки, а фронтенд выскакивает. сообщение «Эту функцию нельзя использовать».

Поэтому откорректируйте приведенный выше код:

String msg = this.process(uri, userKey, mobile, channel, pureToken,);
if (StringUtils.isNotBlank(msg)) {
    
    
    return Mono.error(new CustomException(BlockTypeEnum.getCodeByMsg(msg), msg));
}

Запустите службу шлюза локально, почтальон имитирует запрос и получает возвращаемые данные интерфейса:
Вставьте сюда описание изображения
А? Почему Code — это не код ошибки, соответствующий текстовому сообщению об ошибке, определенному в классе перечисления, а 500?

Посмотрев на код, я обнаружил, что есть класс JsonErrorWebExceptionHandler, который наследует DefaultErrorWebExceptionHandler, тогда этот класс тоже нужно скорректировать:


@Slf4j
public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
    
    

    public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                        ResourceProperties resourceProperties,
                                        ErrorProperties errorProperties,
                                        ApplicationContext applicationContext) {
    
    
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
    
    
        Map<String, Object> errorAttributes = new HashMap<>(8);
        Throwable error = super.getError(request);
        errorAttributes.put("message", error.getMessage());
        if (error instanceof CustomException) {
    
    
            errorAttributes.put("code", ((CustomException) error).getCode());
        } else {
    
    
            errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
        errorAttributes.put("method", request.methodName());
        errorAttributes.put("path", request.path());
        log.warn("网关异常,path:{},method:{},message:{}", request.path(), request.methodName(), error.getMessage());
        return errorAttributes;
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
    
    
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
    
    
        // 这里其实可以根据errorAttributes里面的属性定制HTTP响应码
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

После корректировок и отладки точек останова всё стало нормально:
Вставьте сюда описание изображения
Но! ! !

Позже, при совместной отладке с фронтендом, я обнаружил, что что-то не так:
Вставьте сюда описание изображения
Как показано на картинке выше, в классе перечисления есть несколько кодов, и каждому коду соответствует черный список системных функциональных модулей msg. Раньше я не замечал, что код состояния интерфейса в правом верхнем углу — 500. Это 500 означает, что запрос не удался. Другими словами, если интерфейс не работает и код ответа интерфейса является пользовательским кодом, он бесполезен. Статус должен быть 200, 204 или 202.

Подводя итог: несмотря на то, что Mono.error()возвращается пользовательское бизнес-сообщение об ошибке, код ошибки 9996 преобразуется в 500.

Продолжайте расследование. Найдите статью в ссылке 2 ниже и внесите следующие изменения:

String msg = this.process(uri, userKey, mobile, channel, pureToken,);
if (StringUtils.isNotBlank(msg)) {
    
    
	// 对应200,表明接口请求是成功的,但是触发业务异常错误码
	exchange.getResponse().setStatusCode(HttpStatus.OK);
    return exchange.getResponse()
                        .writeWith(Flux.just(exchange.getResponse()
                                .bufferFactory()
                                .wrap(JSON.toJSONString(msg).getBytes())));
}

В результате отладки Статус в правом верхнем углу становится равным 200:
Вставьте сюда описание изображения
Обратите внимание, что Postman может возвращать сообщения на китайском языке.

После выпуска тестовой среды я обнаружил, что возвращаемые данные в браузере Chrome искажены! ?
Вставьте сюда описание изображения
Как показано на снимке экрана настройки IDEA выше, проблем с искаженным кодом нет, а печать с консоли также работает нормально:
Вставьте сюда описание изображения
искаженный код не является большой проблемой и с ней сталкивались бесчисленное количество раз за многие годы исследований и разработок. Продолжайте исследовать и внесите следующие изменения, чтобы решить проблему:

return exchange.getResponse()
               .writeWith(Flux.just(exchange.getResponse()
               .bufferFactory().wrap(JSON.toJSONString(msg).getBytes(StandardCharsets.UTF_8))));

Получить тело запроса

Как уже говорилось ранее, наше приложение имеет несколько функциональных модулей, и все запросы от одного модуля являются dialog/nextQuestionинтерфейсами. Если пользователь злонамеренно удаляет данные и несколько раз запрашивает этот интерфейс, будет активирован черный список. Однако в нашем дизайне взаимодействия мы надеемся, что пользователи смогут один раз запросить этот интерфейс при переключении с других функциональных модулей на эту функцию. Так откуда ты знаешь, что это первый раз? Затем вам нужно проанализировать requestBody.

В соответствии со статьей 1 ниже добавьте следующий код:

public String resolveBodyForDialog(ServerHttpRequest serverHttpRequest) {
    
    
    String uri = serverHttpRequest.getURI().getPath();
    // 只有某些请求才解析
    if (!StringUtils.equalsAnyIgnoreCase(uri, "dialog/nextQuestion")) {
    
    
        return "";
    }
    StringBuilder sb = new StringBuilder();
    Flux<DataBuffer> body = serverHttpRequest.getBody();
    body.subscribe(buffer -> {
    
    
        byte[] bytes = new byte[buffer.readableByteCount()];
        buffer.read(bytes);
        DataBufferUtils.release(buffer);
        String bodyString = new String(bytes, StandardCharsets.UTF_8);
        sb.append(bodyString);
    });
    return sb.toString();
}

При локальной отладке проблем нет, и requestBody можно получить:
Вставьте сюда описание изображения
Но в тестовой среде есть проблема:
Вставьте сюда описание изображения
подробный журнал ошибок:

500 Server Error for HTTP POST "/api/open/dialog/nextQuestion"
io.netty.handler.codec.EncoderException: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
at io.netty.handler.codec.MessageToMessageEncoder.write(MessageToMessageEncoder.java:107)
at io.netty.channel.CombinedChannelDuplexHandler.write(CombinedChannelDuplexHandler.java:348)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:716)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:708)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:791)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:701)
at reactor.netty.channel.MonoSendMany$SendManyInner.run(MonoSendMany.java:286)
at reactor.netty.channel.MonoSendMany$SendManyInner.trySchedule(MonoSendMany.java:368)
at reactor.netty.channel.MonoSendMany$SendManyInner.onSubscribe(MonoSendMany.java:221)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onSubscribe(FluxContextStart.java:97)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.MonoSubscriberContext.subscribe(MonoSubscriberContext.java:47)
at reactor.core.publisher.FluxSourceMonoFuseable.subscribe(FluxSourceMonoFuseable.java:38)
at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:63)
at reactor.core.publisher.Flux.subscribe(Flux.java:7921)
at reactor.netty.channel.MonoSendMany.subscribe(MonoSendMany.java:81)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.Mono.subscribe(Mono.java:3848)
at reactor.netty.NettyOutbound.subscribe(NettyOutbound.java:305)
at reactor.core.publisher.MonoSource.subscribe(MonoSource.java:51)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:470)
at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.onStateChange(PooledConnectionProvider.java:512)
at reactor.netty.resources.PooledConnectionProvider$PooledConnection.onStateChange(PooledConnectionProvider.java:451)
at reactor.netty.channel.ChannelOperationsHandler.channelActive(ChannelOperationsHandler.java:62)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:204)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelActive(CombinedChannelDuplexHandler.java:414)
at io.netty.channel.ChannelInboundHandlerAdapter.channelActive(ChannelInboundHandlerAdapter.java:69)
at io.netty.channel.CombinedChannelDuplexHandler.channelActive(CombinedChannelDuplexHandler.java:213)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:204)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelActive(DefaultChannelPipeline.java:1396)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.DefaultChannelPipeline.fireChannelActive(DefaultChannelPipeline.java:906)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.fulfillConnectPromise(AbstractEpollChannel.java:618)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.finishConnect(AbstractEpollChannel.java:651)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:527)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:422)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:333)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:906)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)

Изучите конкретно эту статью и внесите следующие коррективы:

String uri = serverHttpRequest.getURI().getPath();
// 只有某些请求才解析
if (!StringUtils.equalsAnyIgnoreCase(uri, DIALOG_URI)) {
    
    
    return "";
}
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
    
    
    CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
    bodyRef.set(charBuffer.toString());
});
return bodyRef.get();

Вышеупомянутое сообщение об ошибке исчезнет. Хотя проблема решена, основная причина не ясна.

Позже была обнаружена искаженная проблема, касающаяся следующего requestBody:

{
    
    
  "stateId": "DASHBOARD",
  "answer": {
    
    
    "transitionId": "GET_HEALTH_ADVICE",
    "label": "开始评估症状"
  }
}

bodyRef.get()Полученные китайские данные искажены. Ссылка 3, решение:

String encoding = System.getProperty("file.encoding");
CharBuffer charBuffer = Charset.forName(encoding).decode(buffer.asByteBuffer());
bodyRef.set(charBuffer.toString());

ссылка

Acho que você gosta

Origin blog.csdn.net/lonelymanontheway/article/details/102291722
Recomendado
Clasificación