Merge pull request 'feat(微服务治理工具): 登录状态异常拦截处理' (#834) from otto/microservices:third-party-tool-forward into third-party-tool-forward

This commit is contained in:
otto 2025-03-06 16:48:57 +08:00
commit 63e9d65b60
4 changed files with 163 additions and 11 deletions

View File

@ -0,0 +1,17 @@
package com.microservices.common.core.constant;
/**
* 异常信息常量
*
* @author otto
*/
public class ExceptionMsgConstants {
/**
* 平台通用异常
*/
public static final String SYSTEM_EXEC_ERROR = "请求处理异常,请联系管理员处理";
/**
* 重试异常
*/
public static final String RETRY_ERROR = "请求发生异常,请重新请求";
}

View File

@ -90,6 +90,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
try { try {
claims = JwtUtils.parseToken(token); claims = JwtUtils.parseToken(token);
} catch (Exception e) { } catch (Exception e) {
log.error("令牌解析失败:{}", e.getMessage());
return unauthorizedResponse(exchange, "令牌格式不正确!"); return unauthorizedResponse(exchange, "令牌格式不正确!");
} }
if (claims == null) { if (claims == null) {

View File

@ -1,9 +1,15 @@
package com.microservices.gateway.filter; package com.microservices.gateway.filter;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.CacheConstants;
import com.microservices.common.core.constant.ExceptionMsgConstants;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.core.web.domain.AjaxResult; import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.redis.service.RedisService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
@ -12,6 +18,7 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
@ -25,16 +32,18 @@ import java.nio.charset.StandardCharsets;
@Slf4j @Slf4j
@Component @Component
public class GlobalResponseFilter implements GlobalFilter, Ordered { public class GlobalResponseFilter implements GlobalFilter, Ordered {
@Autowired
private RedisService redisService;
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath(); String lowerUrl = request.getURI().getPath().toLowerCase();
// 当请求为Nacos时Nacos返回的所有非JSON响应都会被自动转换为结构化的错误信息同时将HTTP状态码设置为200适合用于规范化微服务架构的响应格式
if (url.toLowerCase().startsWith("/nacos")) {
ServerHttpResponse originalResponse = exchange.getResponse(); ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory(); DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 当请求为Nacos时Nacos返回的所有非JSON响应都会被自动转换为结构化的错误信息同时将HTTP状态码设置为200适合用于规范化微服务架构的响应格式
if (lowerUrl.startsWith("/nacos")) {
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override @Override
@ -43,6 +52,22 @@ public class GlobalResponseFilter implements GlobalFilter, Ordered {
if (!status.isError()) { if (!status.isError()) {
return super.writeWith(body); return super.writeWith(body);
} }
if (status == HttpStatus.FORBIDDEN) {
log.error("nacos鉴权异常已清理redis中nacos token");
// 设置新的状态码为 200
setStatusCode(HttpStatus.OK);
// 设置响应内容类型为 JSON
getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 清理Sentinel失效token
redisService.deleteObject(CacheConstants.NACOS_TOKEN);
// 创建自定义的 JSON 响应内容
String jsonBody = JSON.toJSONString(AjaxResult.error(ExceptionMsgConstants.RETRY_ERROR));
byte[] bytes = jsonBody.getBytes(StandardCharsets.UTF_8);
getHeaders().setContentLength(bytes.length);
DataBuffer dataBuffer = bufferFactory.wrap(bytes);
// 返回新的响应内容
return super.writeWith(Mono.just(dataBuffer));
}
return Flux.from(body).<String>handle((dataBuffer, synchronousSink) -> { return Flux.from(body).<String>handle((dataBuffer, synchronousSink) -> {
if (status.isError()) { if (status.isError()) {
@ -71,6 +96,107 @@ public class GlobalResponseFilter implements GlobalFilter, Ordered {
return chain.filter(exchange.mutate().response(decoratedResponse).build()); return chain.filter(exchange.mutate().response(decoratedResponse).build());
} }
if (lowerUrl.startsWith("/portainer")) {
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
HttpStatus status = getStatusCode() != null ? getStatusCode() : HttpStatus.INTERNAL_SERVER_ERROR;
if (!status.isError()) {
return super.writeWith(body);
}
return Flux.from(body).<String>handle((dataBuffer, synchronousSink) -> {
if (status.isError()) {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
//释放掉内存
DataBufferUtils.release(dataBuffer);
String result = new String(bytes, StandardCharsets.UTF_8);
synchronousSink.next(result);
}
synchronousSink.complete();
}).flatMap(resStr -> {
log.error("Portainer响应处理异常异常信息{}", resStr);
originalResponse.setStatusCode(HttpStatus.OK);
byte[] bytes;
if (JSON.isValid(resStr)) {
JSONObject resJson = JSONObject.parseObject(resStr);
if (resJson.containsKey("message")) {
String message = resJson.getString("message");
log.error("Portainer鉴权异常已清理redis中Portainer token");
if (StringUtils.isNotBlank(message) && message.contains("A valid authorisation token is missing")) {
//token失效清除redis缓存
redisService.deleteObject(CacheConstants.PORTAINER_TOKEN);
bytes = JSON.toJSONBytes(AjaxResult.error(ExceptionMsgConstants.RETRY_ERROR));
} else {
bytes = JSON.toJSONBytes(AjaxResult.error(message));
}
} else {
bytes = JSON.toJSONBytes(AjaxResult.error(ExceptionMsgConstants.SYSTEM_EXEC_ERROR));
}
} else {
bytes = JSON.toJSONBytes(AjaxResult.error(ExceptionMsgConstants.SYSTEM_EXEC_ERROR));
}
getHeaders().setContentLength(bytes.length);
DataBuffer buffer = bufferFactory.wrap(bytes);
return super.writeWith(Mono.just(buffer));
}).then();
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
if (lowerUrl.startsWith("/sentinel")) {
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
HttpStatus status = getStatusCode() != null ? getStatusCode() : HttpStatus.INTERNAL_SERVER_ERROR;
if (!status.isError()) {
return super.writeWith(body);
}
if (status == HttpStatus.UNAUTHORIZED) {
log.error("sentinel鉴权异常已清理redis中sentinel token");
// 设置新的状态码为 200
setStatusCode(HttpStatus.OK);
// 设置响应内容类型为 JSON
getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 清理Sentinel失效token
redisService.deleteObject(CacheConstants.SENTINEL_TOKEN);
// 创建自定义的 JSON 响应内容
String jsonBody = JSON.toJSONString(AjaxResult.error(ExceptionMsgConstants.RETRY_ERROR));
byte[] bytes = jsonBody.getBytes(StandardCharsets.UTF_8);
getHeaders().setContentLength(bytes.length);
DataBuffer dataBuffer = bufferFactory.wrap(bytes);
// 返回新的响应内容
return super.writeWith(Mono.just(dataBuffer));
}
return Flux.from(body).<String>handle((dataBuffer, synchronousSink) -> {
if (status.isError()) {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
//释放掉内存
DataBufferUtils.release(dataBuffer);
String result = new String(bytes, StandardCharsets.UTF_8);
synchronousSink.next(result);
}
synchronousSink.complete();
}).flatMap(resStr -> {
log.error("Sentinel响应处理异常异常信息{}", resStr);
byte[] bytes;
bytes = JSON.toJSONBytes(AjaxResult.error(ExceptionMsgConstants.SYSTEM_EXEC_ERROR));
setStatusCode(HttpStatus.OK);
getHeaders().setContentLength(bytes.length);
DataBuffer buffer = bufferFactory.wrap(bytes);
return super.writeWith(Mono.just(buffer));
}).then();
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange); return chain.filter(exchange);
} }

View File

@ -3,6 +3,7 @@ package com.microservices.gateway.service.impl;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.CacheConstants; import com.microservices.common.core.constant.CacheConstants;
import com.microservices.common.core.constant.ExceptionMsgConstants;
import com.microservices.common.core.constant.TokenConstants; import com.microservices.common.core.constant.TokenConstants;
import com.microservices.common.core.exception.ServiceException; import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.CookieUtil; import com.microservices.common.core.utils.CookieUtil;
@ -60,12 +61,12 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
*/ */
@Override @Override
public Mono<Void> handleRequestForThirdPartyTools(ServerWebExchange exchange, ServerHttpRequest request, ServerHttpRequest.Builder mutate) { public Mono<Void> handleRequestForThirdPartyTools(ServerWebExchange exchange, ServerHttpRequest request, ServerHttpRequest.Builder mutate) {
String url = request.getURI().getPath(); String lowerUrl = request.getURI().getPath().toLowerCase();
if (StringUtils.isEmpty(url)) { if (StringUtils.isEmpty(lowerUrl)) {
return null; return null;
} }
// 处理Sentinel请求 // 处理Sentinel请求
if (url.toLowerCase().startsWith("/sentinel")) { if (lowerUrl.startsWith("/sentinel")) {
// 检查Cookie中是否携带sentinel cookie // 检查Cookie中是否携带sentinel cookie
String cookie = request.getHeaders().getFirst(TokenConstants.Cookie); String cookie = request.getHeaders().getFirst(TokenConstants.Cookie);
if (StringUtils.isNotEmpty(CookieUtil.getCookieValue(cookie, TokenConstants.Sentinel_Token_Key))) { if (StringUtils.isNotEmpty(CookieUtil.getCookieValue(cookie, TokenConstants.Sentinel_Token_Key))) {
@ -98,7 +99,7 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
} }
} }
// 处理Nacos请求 // 处理Nacos请求
if (url.toLowerCase().startsWith("/nacos")) { if (lowerUrl.startsWith("/nacos")) {
String nacosToken = null; String nacosToken = null;
if (redisService.hasKey(CacheConstants.NACOS_TOKEN)) { if (redisService.hasKey(CacheConstants.NACOS_TOKEN)) {
nacosToken = redisService.getCacheObject(CacheConstants.NACOS_TOKEN); nacosToken = redisService.getCacheObject(CacheConstants.NACOS_TOKEN);
@ -117,6 +118,9 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
return exceptionResponse(exchange, str, res.status()); return exceptionResponse(exchange, str, res.status());
} }
JSONObject resJsonObject = JSONObject.parseObject(str); JSONObject resJsonObject = JSONObject.parseObject(str);
if (resJsonObject == null) {
return exceptionResponse(exchange, str, res.status());
}
nacosToken = resJsonObject.getString("accessToken"); nacosToken = resJsonObject.getString("accessToken");
Long tokenTtl = resJsonObject.getLong("tokenTtl"); Long tokenTtl = resJsonObject.getLong("tokenTtl");
if (nacosToken != null && tokenTtl != null) { if (nacosToken != null && tokenTtl != null) {
@ -136,7 +140,8 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
} }
// 处理portainer请求 // 处理portainer请求
if (url.toLowerCase().startsWith("/portainer")) { if (lowerUrl.startsWith("/portainer")) {
String portainerToken = null; String portainerToken = null;
if (redisService.hasKey(CacheConstants.PORTAINER_TOKEN)) { if (redisService.hasKey(CacheConstants.PORTAINER_TOKEN)) {
portainerToken = redisService.getCacheObject(CacheConstants.PORTAINER_TOKEN); portainerToken = redisService.getCacheObject(CacheConstants.PORTAINER_TOKEN);
@ -156,6 +161,9 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
} }
JSONObject resJsonObject = JSONObject.parseObject(str); JSONObject resJsonObject = JSONObject.parseObject(str);
portainerToken = resJsonObject.getString("jwt"); portainerToken = resJsonObject.getString("jwt");
if (portainerToken == null) {
return exceptionResponse(exchange, str, res.status());
}
String[] tokenSplit = portainerToken.split("\\."); String[] tokenSplit = portainerToken.split("\\.");
String tokenBodyStr = new String(Base64.getDecoder().decode(tokenSplit[1]), StandardCharsets.UTF_8); String tokenBodyStr = new String(Base64.getDecoder().decode(tokenSplit[1]), StandardCharsets.UTF_8);
JSONObject tokenBody = JSONObject.parseObject(tokenBodyStr); JSONObject tokenBody = JSONObject.parseObject(tokenBodyStr);
@ -183,7 +191,7 @@ public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
private Mono<Void> exceptionResponse(ServerWebExchange exchange, String msg, int code) { private Mono<Void> exceptionResponse(ServerWebExchange exchange, String msg, int code) {
log.error("[第三方工具请求处理异常]请求路径:{}", exchange.getRequest().getPath()); log.error("[第三方工具请求处理异常]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), msg);
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, code); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), ExceptionMsgConstants.SYSTEM_EXEC_ERROR, code);
} }
} }