A、技术栈
B、本节实现目标
@RestControllerAdvice
拦截Controller返回统一格式数据@ControllerAdvice
拦截返回统一格式ExceptionAPI 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
客户端会多次请求不同的微服务,增加了客户端的复杂性。
存在跨域请求,在一定场景下处理相对复杂。
认证复杂,每个服务都需要独立认证。
难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
API 网关
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
由于Spring 5.0支持 Netty,Http2,而Spring Boot 2.0支持Spring 5.0,因此Spring Cloud Gateway支持 Netty和Http2。
补充:
1、Zuul(1.x) 基于 Servlet,使用阻塞 API,它不支持任何长连接 ,如 WebSockets。
2、Zuul(2.x) 基于Netty。
3、Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型,支持 WebSockets,支持限流等新特性。
4、Spring Cloud 已经不再集成 Zuul 2.x 。
认证服务(mall-auth)负责认证授权,网关服务(mall-gateway)负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
具体服务:
新建mall-gateway服务用户token鉴权、API请求转发
4.0.0
mall-pom
com.ac
1.0-SNAPSHOT
com.ac
mall-gateway
${mall.version}
mall-gateway
网关服务
com.ac
mall-core
1.0-SNAPSHOT
com.ac
mall-oauth2-module
1.0-SNAPSHOT
org.springframework
spring-webmvc
test
org.springframework.cloud
spring-cloud-starter-gateway
4.0.4
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
org.springframework.security
spring-security-oauth2-resource-server
org.apache.maven.plugins
maven-compiler-plugin
8
bootstrap-dev.yml
server:
port: 6001
spring:
application:
name: mall-gateway
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: dev_id
file-extension: yml
shared-configs:
- data-id: common.yml
group: DEFAULT_GROUP
refresh: true
discovery:
namespace: dev_id
gateway:
routes:
- id: mall-member-route # 当前路由的标识, 要求唯一
uri: lb://mall-member # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/mall-member/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters:
- StripPrefix=1 # 转发之前去掉1层路径
- id: mall-search-route
uri: lb://mall-search
predicates:
- Path=/mall-search/**
filters:
- StripPrefix=1
- id: mall-product-route
uri: lb://mall-product
predicates:
- Path=/mall-product/**
filters:
- StripPrefix=1
- id: mall-order-route
uri: lb://mall-order
predicates:
- Path=/mall-order/**
filters:
- StripPrefix=1
#gateway swagger开关
swagger:
enable: true
#配置白名单路径
mall:
security:
ignore:
urls:
- "/**/member/list"
- "/**/redis/**"
重点说明一下配置,- StripPrefix=1
转发之前去掉1层路径,如:127.0.0.1:6001/mall-member/member/264260572479489
,去掉第一层路径mall-member
,就变成了127.0.0.1:6001/member/264260572479489
,会被转发到mall-member服务。
mall-core服务config包里的WebMvcConfigurer配置类,和mall-gateway服务里排除的spring-webmvc
有冲突,因此排除该目录下的配置类
@ComponentScan(
value = "com.ac.*",
excludeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.ac.core.config.*")})
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
package com.ac.gateway.config;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
@Configuration
@Primary
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
public class GateWaySwaggerConfig implements SwaggerResourcesProvider {
public static final String API_URI = "/v2/api-docs";
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
public GateWaySwaggerConfig(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
this.routeLocator = routeLocator;
this.gatewayProperties = gatewayProperties;
}
@Override
public List get() {
List resources = new ArrayList<>();
List routes = new ArrayList<>();
//取出gateway的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//结合配置的route-路径(Path),和route过滤,只获取有效的route节点
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("/**", API_URI)))));
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("1.0");
return swaggerResource;
}
}
package com.ac.gateway.controller;
import com.ac.gateway.config.GateWaySwaggerConfig;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import javax.annotation.Resource;
/**
* @author Alan Chen
* @description 在浏览器中打开gateway的swagger地址时,会将请求自动打到下面API
* http://127.0.0.1:6001/swagger-ui.html
* @date 2023/02/22
*/
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
@RestController
public class SwaggerController {
@Resource
private GateWaySwaggerConfig gateWaySwaggerConfig;
@GetMapping("/swagger-resources/configuration/security")
public Mono> securityConfiguration() {
return Mono.just(new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK));
}
@GetMapping("/swagger-resources/configuration/ui")
public Mono> uiConfiguration() {
return Mono.just(new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK));
}
@GetMapping("/swagger-resources")
public Mono swaggerResources() {
return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
}
@GetMapping("/")
public Mono swaggerResourcesN() {
return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
}
@GetMapping("/csrf")
public Mono swaggerResourcesCsrf() {
return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
}
}
在GateWaySwaggerConfig、SwaggerController类上都加上了@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
注解,该注解表示当swagger.enable配置值为true时,则将当前类初始化为bean。该开关用户关闭生产环境swagger,保证服务安全性。
下拉选择服务
@RestControllerAdvice
拦截Controller返回统一格式数据该配置类放在mall-core模块
package com.ac.core.config;
import com.ac.core.response.RepResult;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.ArrayList;
import java.util.List;
/**
* @author Alan Chen
* @description Controller返回参数全局包装成ResponseResult对象
* 使用是一般需要指定basePackages,@RestControllerAdvice(basePackages = {"com.netx.web.controller"})
* 只拦截controller包下的类;否则swagger也会拦截影响swagger正常使用
* @date 2023/04/15
*/
@EnableWebMvc
@Configuration
@RestControllerAdvice
public class GlobalReturnConfig implements ResponseBodyAdvice
查询用户接口
虽然查询用户接口,返回的是一个用户对象,但返回到前端时,统一返回的是RepResult格式,将用户数据放在了data里。
统一返回RepResult格式
@ControllerAdvice
拦截返回统一格式Exception该配置放在mall-core里
package com.ac.core.exception;
import com.ac.core.i18n.I18nResource;
import com.ac.core.response.RepResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author Alan Chen
* @description 全局异常处理
* @date 2023/4/27
*/
@Slf4j
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
private I18nResource validationI18nSource;
private I18nResource responseMessageI18nSource;
/**
* 是否开启Validator国际化功能
* @return
*/
protected boolean enableValidationI18n(){
return false;
}
/**
* 国际化文件地址
* @return
*/
protected String validationI18nSourcePath(){
return "i18n/validation";
}
/**
* 是否开启消息国际化
* @return
*/
protected boolean enableResponseMessageI18n(){
return false;
}
/**
* 消息国际化文件地址
* @return
*/
protected String responseMessageI18nSourcePath(){
return "i18n/messages";
}
/**
* 全局异常捕捉处理
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public RepResult errorHandler(Exception ex) {
ex.printStackTrace();
log.error("Exception:"+ex.getMessage());
return RepResult.fail(ex.getMessage());
}
/**
* validator校验失败信息处理
* @param exception
* @return
*/
@ResponseBody
@ExceptionHandler(value = BindException.class)
public RepResult bindExceptionHandler(BindException exception) {
exception.printStackTrace();
return doValidationException(exception.getBindingResult());
}
/**
* validator校验失败信息处理
* @param exception
* @return
*/
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public RepResult validationHandler(MethodArgumentNotValidException exception) {
exception.printStackTrace();
log.error("MethodArgumentNotValidException:"+exception.getMessage());
return doValidationException(exception.getBindingResult());
}
/**
* 拦截捕捉业务异常 ServiceException.class
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value = ServerException.class)
public RepResult commonExceptionHandler(ServerException ex) {
ex.printStackTrace();
log.error("ServiceException:"+ex.getMessage());
if(enableResponseMessageI18n()){
if(responseMessageI18nSource == null){
responseMessageI18nSource = new I18nResource(responseMessageI18nSourcePath());
}
String messageKey = ex.getMessage();
try{
String message = responseMessageI18nSource.getValue(messageKey);
String[] placeholder = ex.getPlaceholder();
if(placeholder!=null && placeholder.length>0){
for(int i =0;i
统一异常格式
请求gateway访问mall-member服务接口,不携带token,请求被拦截
鉴权拦截成功
请求gateway访问mall-member服务接口,携带合法token,请求被正确转发
鉴权成功转发请求
在bootstrap-dev.yml里配置了白名单:
#配置白名单路径
mall:
security:
ignore:
urls:
- "/**/member/list"
- "/**/redis/**"
请求gateway访问mall-member服务白名单接口,不携带token,请求被正确转发
访问白名单接口