SpringGateWay+sentinel实现网关服务
了解一下大致的流程图
可以看见网关服务含有服务路由、流量控制、熔断保护、日志监控、安全认证等功能。
注意:网关服务这些功能,不一定都要实现,通常服务路由、流量控制、熔断保护、安全认证是必需的,日志监控是非必需。具体根据实际项目需求实现。
这些功能是如何运转的呢?
答:首先我们构建项目,最好把认证服务拆开成一个单独模块,这样可以避免耦合度高。
拆开之后,分成二部分,第一部分网关服务(负责转发,流控、熔断、路由),第二部分认证服务(负责认证、鉴权)。
提示:客户端进入API GateWay后执行的顺序分别是安全认证、流量控制、熔断保护、服务路由、日志监控。
本文章主将第一部分、认证服务部分在:🚀SpringSecurity+OAuth2+JWT认证服务 - 掘金 (juejin.cn)
各功能的简单介绍
转发
将请求实际发送到目标服务的操作。
路由
将请求路由到对应的后端服务。它能够根据请求的路径、方法、头部信息等进行匹配和映射,确保请求被正确地转发到相应的服务, 由 ID, URI,断言(predicates)和过滤器(filters)定义。如果断言为真,则路由匹配。
-
优点:使得后端服务的管理和扩展更加灵活和简便。
-
注意: 路由是决定请求应该被发送到哪个目标服务的规则,而转发是将请求实际发送到目标服务的操作。通俗来讲、上图箭头的指向就是路由做的,而通过箭头发送请求是转发做的。
流控
流量控制功能允许您对请求进行限流,防止过载和故障。通过设定阈值和策略,可以控制请求的数量或速率,确保后端服务不会被过多的请求压垮。
-
优点:维持系统的稳定性和可靠性。
-
注意:网关流控对微服务之间的调用无法有效,还得设置普通流控哦。
熔断
熔断保护是一种分布式系统中的故障保护机制。它能够监控后端服务的状态,在服务出现故障或请求压力过大时,断开与该服务的连接,提供降级或默认响应。
- 优点:熔断保护可以防止故障扩大,提高系统的可用性和稳定性。
认证
安全认证功能用于验证请求的身份信息,确保只有经过身份验证的用户可以访问后端服务。它可以提供不同的身份验证机制,如 API 密钥、OAuth、JWT 等。
- 优点:保护系统免受未经授权的访问和潜在的安全威胁。
举例说明
下面我们举例一个网关模块,该例子实现了网关服务的路由,转发,流控。
注意:网关服务不是只在网关模块的,例如用户模块里是可以有熔断降级功能的,这个功能属于网关服务的一部分,举例的网关模块只包含路由和流控,
而熔断降级功能是通过 Sentinel整合Feign实现的,直接在@FeignClient配置fallback实现降级措施,这部分后续会说明。
这里使用的是nacos作为服务注册中心和配置中心。
nacos中的网关配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 启用服务发现
lower-case-service-id: true
routes:
- id: 认证中心
uri: lb://youlai-auth
predicates:
- Path=/youlai-auth/**
filters:
- StripPrefix=1
- id: 系统服务
uri: lb://youlai-system
predicates:
- Path=/youlai-system/**
filters:
- StripPrefix=1
- TokenRelay
- id: 订单服务
uri: lb://mall-oms
predicates:
- Path=/mall-oms/**
filters:
- StripPrefix=1
- TokenRelay
- id: 商品服务
uri: lb://mall-pms
predicates:
- Path=/mall-pms/**
filters:
- StripPrefix=1
- TokenRelay
- id: 会员服务
uri: lb://mall-ums
predicates:
- Path=/mall-ums/**
filters:
- StripPrefix=1
- TokenRelay
- id: 营销服务
uri: lb://mall-sms
predicates:
- Path=/mall-sms/**
filters:
- StripPrefix=1
- TokenRelay
- id: 实验室
uri: lb://laboratory
predicates:
- Path=/laboratory/**
filters:
- StripPrefix=1
- TokenRelay
sentinel:
enabled: true
eager: true #表示Sentinel的初始化过程将会在应用程序启动时立即执行。
transport: #定义Sentinel的传输配置
dashboard: ${sentinel.dashboard} #仪表板地址,就是好像localhost这种地址
port: 8719 #端口
datasource: #数据源
# 网关限流规则,gw-flow为key,随便定义
gw-flow:
nacos: #数据源选了nacos
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-flow-rules
groupId: SENTINEL_GROUP
rule-type: gw-flow #选用限流规则
# 自定义API分组,gw-api-group为key,随便定义,API指类似"/user",这就是API接口。
gw-api-group:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-api-group-rules
groupId: SENTINEL_GROUP
rule-type: gw-api-group #选用自定义api分组,据实际需求,对每个API分组设置独立的限流规则,例如限制每秒钟允许通过的请求数量、限制每分钟的并发请求数量等等
# Feign 配置
feign:
httpclient:
enabled: true
okhttp:
enabled: false
sentinel: # 开启feign对sentinel的支持
enabled: false
# 禁止访问路径
security:
forbiddenURIs:
# 获取用户认证信息
- /youlai-system/api/v1/users/{username}/authinfo
需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能,这是源码规定的固定写法。
配置解释:
locator:enable:true这个会启用服务发现和路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务。
- 注意:上面的代码里面配置了路由规则(routes),手动配置如果跟自动配置冲突就会覆盖自动配置。其中手动配置看的是uri,自动配置看的是服务ID,比较的是这两者哦!
- 例如上面代码的认证中心,访问
http://localhost:9999/youlai-gateway/youlai-auth/**就可以将请求转发过去了。其中的youlai-auth就是服务ID或者你手动配置的uri。
- lower-case-service-id: true这个会将服务ID转换为小写,因为有些服务注册中心对服务ID的查询接口要求小写,所以开了无妨。 - 接下来是routes的id,uri,predicates,filters。
- id和uri就不多解释了。
predicates:是匹配规则(术语叫断言),指请求需满足predicates的条件才能被这个路由匹配。 匹配内容可以根据时间、Cookie、Header、Host、请求方式、请求路径、请求参数、请求IP地址来匹配,可以混合使用。 例如上面的代码里面认证中心是根据请求路径匹配,就是说符合/youlai-auth/**的就会使用这个路由。
filters:过滤器,有添加前缀(PrefixPath),StripPrefix(路径截取)AddRequestParameter(在请求头添加指定参数)等。
- 例如上面的代码里面认证中心是截取路径1个,过滤后访问地址变为:http://localhost:9999/youlai-auth/**
- 补充:
http://localhost:9999/是目标服务的基础URL,StripPrefix过滤器不影响这部分,请求路径为/youlai-gateway/youlai-auth/**, - 而截取路径从第一个路径段开始截。
- sentinel:作用用来流量控制和熔断降级。这里仅配置流量控制,具体配置解释看上面的代码就行。在sentinel设置流量控制,详细的sentinel内容不在这过多解释。
-
- 下面是限流规则
[ { "resource": "ReactiveCompositeDiscoveryClient_youlai-auth", "resourceMode": 0, "count": 3, "grade": 1 }]
resouce:资源名:默认前缀+服务名称
resouceMode:针对资源类型:0是网关route
count:QPS阔值
grant:限流类型 ,1指QPS限流
完成nacos配置,接下来展示网关模块的配置类:
OAuth2ClientSecurityConfig OAuth客户端安全配置
@ConfigurationProperties(prefix = "security")
@EnableWebFluxSecurity//启用了 WebFlux 安全性
@Slf4j
public class OAuth2ClientSecurityConfig {
/**
* 禁用访问路径集合
*/
@Setter
private List<String> forbiddenURIs;//@ConfigurationProperties(prefix = "security")这个注解会把对应的属性注入到这里
//通过 securityWebFilterChain 方法配置了一些安全规则
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http
) {
if (CollectionUtil.isEmpty(forbiddenURIs)) {
forbiddenURIs = Collections.EMPTY_LIST;
}
http.authorizeExchange()//这是一个用于定义访问控制规则的方法。你可以使用它来配置特定的请求路径的访问权限。
.pathMatchers(Convert.toStrArray(forbiddenURIs)).denyAll()//这里使用 pathMatchers 方法指定一组禁止访问的 URI。Convert.toStrArray(forbiddenURIs) 是将 forbiddenURIs 转换为字符串数组。.denyAll() 表示对这些路径的访问将被完全拒绝。
// 放行交由资源服务器进行认证鉴权
.anyExchange().permitAll()//方法用于指定任何其他未特别匹配的请求都允许访问。.permitAll() 表示对这些路径的访问将被完全允许。
.and() // 这是用于链接多个安全规则的方法。
// 禁用csrf token安全校验
.csrf().disable();//这部分禁用了 CSRF(csrf()) 防护功能,CSRF 是一种常见的 Web 应用程序攻击,通过禁用它可以简化开发过程。
return http.build();//该语句用于构建并返回 SecurityWebFilterChain 实例,用于保护 WebFlux 应用程序的路由和端点。
}
//用于配置跨域资源共享(CORS)的方法
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// UrlBasedCorsConfigurationSource是Spring 提供的基于 URL 的 CORS 配置源。
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
CorsConfiguration corsConfig = new CorsConfiguration();//创建示例用于配置CORS规则
// 允许所有请求方法
corsConfig.addAllowedMethod("*");
// 允许所有域,当请求头
corsConfig.addAllowedOriginPattern("*");
// 允许全部请求头
corsConfig.addAllowedHeader("*");
// 允许携带 Authorization 头
corsConfig.setAllowCredentials(true);
// 允许全部请求路径
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
}
这个类整合了配置请求路径:
-
1、通过SecurityWebFilterChain来配置一组禁止访问的路径,还有一些其它安全规则的配置看代码注释就好。
- 注意:.anyExchange().permitAll()是指除了禁止的路径,其它路径放行,这里并没有把鉴权管理器(ReactiveAuthorizationManager)加入, 所以这个网关模块没有鉴权,鉴权在下游的各个微服务里面调用,后面写微服务的时候会提及。
-
2、通过CorsConfigurationSource来配置跨域资源共享(CORS)的方法
SentinelConfiguration 自定义网关流控异常
@Configuration
public class SentinelConfiguration {
//作用是重新定义限流之后的异常改为中文
@PostConstruct //使用该实例之前,相关的初始化工作已经完成
private void initBlockHandler() {
BlockRequestHandler blockRequestHandler = (exchange, t) ->
ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(ResultCode.FLOW_LIMITING.toString())
);
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
这个类就是自定义网关流控异常的配置,BlockRequestHandler 是 Sentinel 库中的一个接口,用于定义限流后的请求阻塞处理逻辑, 请求体的 BodyInserter 是用于表示请求体数据的内容,这里将限流错误代码的字符串表示作为请求体的 BodyInserter, 当被限流时,将返回一个状态码为429的响应,响应的内容类型为JSON,且响应体中的数据是限流错误的代码或消息。
然后展示网关模块的处理类和路由类:
CaptchaHandler 验证码处理器
@Component
@RequiredArgsConstructor
public class CaptchaHandler implements HandlerFunction<ServerResponse> {
private final StringRedisTemplate redisTemplate;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
// 获取验证码
GifCaptcha captcha = CaptchaUtil.createGifCaptcha(120, 40, 4); // 宽、高、位数,方法用于生成一个 GIF 验证码
String captchaCode = captcha.getCode(); // 验证码
String captchaBase64 = captcha.getImageBase64Data(); // 验证码图片Base64
// 验证码文本缓存至Redis,用于登录校验
//简化版的 UUID,它使用了基于当前时间、IP地址、线程ID等信息生成一个32位的16进制字符串,没有连字符
String verifyCodeKey = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(SecurityConstants.VERIFY_CODE_KEY_PREFIX + verifyCodeKey, captchaCode,
120, TimeUnit.SECONDS);
Map<String, String> result = new HashMap<>(2);
result.put("verifyCodeKey", verifyCodeKey);
result.put("verifyCodeImg", captchaBase64);
return ServerResponse.ok().body(BodyInserters.fromValue(Result.success(result)));
}
}
这个类主要内容:通过huTool工具包的CaptchaUtil来获取一个GIF验证码,然后通过huTool工具包的IdUtil来获取一个UUID作为key, 验证码的值captchaCode作为value和生成的verifyCodeKey作为key存入redis中并设置过期时间, 而验证码图片通过map集合放到响应体中,在前端显示。
因为是验证码处理,所以业务逻辑也写在这了。路由类可以直接调用这个方法,然后返回响应结构。
CaptchaRouter 验证码路由
@Configuration
@RequiredArgsConstructor
public class CaptchaRouter {
private final CaptchaHandler captchaHandler;
//“predicates” 字段用于定义路由规则,断言的意思
//“uri” 字段是用来指定该路由的转发目标
@Bean
public RouterFunction<ServerResponse> captchaRouterFunction() {
return RouterFunctions
.route(RequestPredicates.GET("/captcha")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), captchaHandler::handle);
}
}
这个类主要内容: 定义了一个路由函数,创建了一个路由规则,路由规则定义了GET请求的断言条件,并且路径为/captcha才会被匹配
通过and可以链式添加断言条件,将目标请求的 Accept 头部与 MediaType.TEXT_PLAIN 进行匹配,只有满足这个条件的请求才会被匹配。
提示: MediaType.TEXT_PLAIN表示纯文本类型的媒体类型(Media Type)之一,表示只接受媒体类型为纯文本的请求。
如果请求满足上述两个条件,则将请求路由到 captchaHandler::handle 方法进行处理。
总结
-
首先网关服务会先进行认证,认证服务被拆分成一个单独模块,认证模块完成了认证、鉴权功能,而网关则完成了路由、流控、熔断功能。
-
网关模块只含有路由,转发,流控功能,熔断功能是通过sentinel整合feign实现在其它微服务(如用户)模块设置,
-
不在网关模块设置而在其它微服务模块设置熔断可以增强系统的容错能力和稳定性,提升用户体验。
-
鉴权可以在网关模块设置,也可以在其它微服务模块设置,通过springsecurity的注解@PreAuthorize@PostAuthorize或者自定义一个权限控制注解和这个注解的处理类,然后通过在要拦截的接口上加这个自定义注解来实现拦截鉴权功能。
-
流控功能可以在sentinel上设置或者nacos配置限流规则来实现网关的流控。