API 网关的出现的原因是微服务架构的出现,不同的微服务一般会有不同的服务地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理相对复杂。
- 认证复杂,每个服务都需要独立认证。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
- 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做 ,所以网关的性能,高可用,安全性都是至关重要的。
备注:Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务
常用网关有哪些 ?
Nginx、Kong、ZUUL、Spring Cloud Gateway(Spring Cloud 官方)、Linkerd 等
Spring Cloud Zuul
Zuul 是 Netflix 开源的微服务网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器 (比如:动态路由)。Spring Cloud Zuul 对 Zuul 进行了整合 ,从而更方便的与 Spring Cloud 一起使用。
Zuul1
Zuul1 是基于 Servlet 框架构建,采用的是阻塞和多线程方式,即一个线程处理一次连接请求,这种方式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发生。
Zuul2
Zuul2 与 Zuul1 最大的区别是它运行在异步和无阻塞框架上,每个 CPU 核一个线程,处理所有的请求和响应,请求和响应的生命周期是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较小。又由于数据被存储在同一个 CPU 里,可以复用 CPU 级别的缓存,前面提及的延迟和重试风暴问题也通过队列存储连接数和事件数方式减轻了很多(较线程切换来说轻量级很多,自然消耗较小)。这一变化一定会大大提升性能。
注:zuul 2.0 版本 Spring Cloud 官方现阶段不打算集成,官方还是推荐使用 Spring Cloud Gateway
性能
可以参考:纠错帖:Zuul & Spring Cloud Gateway & Linkerd性能对比 ,简单来说,Zuul 1.x 是一个基于阻塞 IO 的 API Gateway,另外 Spring Cloud Gateway 性能很好。
高可用
一般生产环境需要将多个 Zuul 节点注册到 Eureka Server 上,就可以实现 Zuul 的高可用。事实上,这种情况下的高可用和其他服务做高可用(例如:Eurka Server 集群)的方案没有什么区别。当 Zuul 客户端注册到 Eureka Server 上时,Zuul 客户端会自动从 Eureka Server 查询 Zuul Server 列表,然后使用负载均衡组件(例如: Ribbon)请求 Zuul 集群。另外的方式也可以使用 Nginx 或者硬件 F5 的来实现。
安全性
Spring Cloud 的微服务化后,一般可以使用 Spring Cloud Security 结合 OAuth2.0,生成的 Token 采用 JWT 来验证票据,但 Spring Cloud Security 暂时还不支持 OpenID Connect 协议。Zuul 将自己注册为 Eureka 服务治理下,同时也从 Eureka 服务治理中获得所有其他微服务的实例信息。通过搭建独立的 OAuth2 认证授权服务,将微服务单独剥离出来,这些认证与微服务自己的业务并没有太大的关系,所以这些功能完全可以独立成一个单独的服务存在。独立出来之后,并不是给每个微服务调用(业务服务一般在内网),而是通过 API网关进行统一调用,来对微服务接口做前置过滤,实现对分布式系统中的其他的微服务接口的拦截和安全校验。
创建 Zuul 网关服务
Maven
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-zuulartifactId> dependency> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId> dependency>
@SpringBootApplication //@EnableOAuth2Sso @EnableZuulProxy public class MicrosrvZuulGatewayApplication { public static void main(String[] args) { SpringApplication.run(MicrosrvZuulGatewayApplication.class, args); } }
application.yml
spring: application: name: microsrv-zuul-gateway server: port: 5555 eureka: instance: preferIpAddress: true client: serviceUrl: defaultZone: http://10.255.131.162:8000/eureka/,http://10.255.131.163:8000/eureka/,http://10.255.131.164:8000/eureka/ zuul: host: connect-timeout-millis: 20000 socket-timeout-millis: 20000 ignoredServices: '*' prefix: /api # 设置一个公共的前缀 routes: auth-service: path: /auth/** sensitiveHeaders: serviceId: idsrv-server order-service: path: /order/** sensitiveHeaders: serviceId: order-service add-proxy-headers: true
因为使用 Eureka 来服务发现,所以请求URL格式形如 /service-id/** 会被自动转发到在 Eureka Server 上注册的 service id 为“service-id”的微服务应用上。例如上面我们定义了两个路由规则,比如将“order-service”的请求转发到相应 service-id 注册的服务上,也可以通过修改 zuul.prefix=/api 配置来配置全局的前缀地址。默认 Eureka Server会暴露所有注册在它上面的微服务。你可以使用 zuul.ignored-services 属性来禁止这种行为,且只有显式配置的服务才会被暴露。
Zuul 整合 OAuth2.0 认证授权
Zuul 整合 OAuth2.0 有两种思路,一种是授权服务器采用 JwtToken 统一在网关层使用公钥验证票据,判断权限等操作;另一种是让资源端处理,网关只做路由转发。
资源端配置
maven
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.security.oauthgroupId> <artifactId>spring-security-oauth2artifactId> <version>2.3.3.RELEASEversion> dependency> <dependency> <groupId>org.springframework.security.oauth.bootgroupId> <artifactId>spring-security-oauth2-autoconfigureartifactId> <version>2.0.5.RELEASEversion> dependency>
Spring Boot
@SpringBootApplication @EnableResourceServer public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } @RestController public class AccountController { @GetMapping("/principal") @PreAuthorize("hasAnyAuthority('user')") public Principal user(Principal principal) { return principal; } @GetMapping("/query") @PreAuthorize("hasAnyAuthority('all')") public String all () { return "具有 all 权限"; } }
application.yml
logging: level: org.springframework: DEBUG server: port: 5000 security: oauth2: resource: # prefer-token-info: true # user-info-uri: http://localhost:8080/api/v1/users/principal # token-info-uri: http://localhost:8080/oauth/check_token jwt: # key-uri: http://localhost:8080/oauth/token_key key-value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4 g4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG Pr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN sTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp o4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8 clps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp 8wIDAQAB -----END PUBLIC KEY-----
最后可以在 Zuul 上启用 @EnableOAuth2Sso 注解作为 OAuth2.0 的一个客户端(非必须),这样当用户访问到网关没有授权的话,会跳转到授权服务器登录授权。
security: oauth2: client: access-token-uri:http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize client-id: client_test client-secret: secret_test resource: user-info-uri: http://localhost:8080/api/v1/users/principal prefer-token-info: false
REFER:
https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/