目录
一、微服务网关介绍
1.1 存在的问题
在分布式项目中,不同的微服务有不同的网络地址,而外部的客户端完成一个业务需求时,可能涉及到多个微服务接口,若让客户端直接与多个微服务通信,会带来以下问题:
- 业务复杂,客户端会多次请求不同的微服务,增加了业务处理时间和业务复杂度
- 跨域问题
- 认证复杂,各个微服务都需要先对用户的请求进行身份认证
- 安全问题,直接将各个微服务模块都暴露给客户端
- 访问困难,根据实际情况,部分微服务设置了防火墙等方式,无法直接访问
- 重构复杂,随着项目咖啡啊,若项目需要重新划分微服务,如将多个微服务合并成一个微服务,或是拆分微服务,若是由客户端直接与微服务通信,则会导致工作复杂度增加
1.2 微服务网关
1.2.1 概述
使用微服务网关即能解决上面的问题。
网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过网关,这就可以形成这样的功能架构:
- 网关:处理安全、性能、监控方面的业务
- 业务微服务:更专注于业务逻辑的实现
整体架构图如下:
1.2.2 优点
- 降低了业务复杂度,让客户端只需发起一次请求到网关,由网关根据业务需求请求各个微服务处理,并将结果返回给客户端。
- 可以统一解决跨域问题
- 能够在网关服务中统一处理认证、日志、监控、限流
- 提高了安全性,由统一的访问入口,降低了业务服务的暴露风险和服务器受攻击面积
- 便于重构,客户端只负责向网关请求,实际的后端业务处理由后端自己完成,即若修改了后端业务逻辑,无需升级客户端,降低了前后端的耦合性。
1.2.3 常用技术
常见的实现微服务网关的技术有:
- nginx:高性能的HTTP和方向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
- zuul:由 Netflix 出品的基于JVM路由和服务器的负载均衡器
- spring-cloud-gateway:由 spring 出品的基于spring 的网关项目,集成断路器、路径重写等
由于spring-cloud-gateway 的性能优于 zuul,且无缝兼容 springboot 等框架的项目,故市场大部分使用 spring-cloud-gateway
1.2.4 实际框架
在实际项目使用中,微服务网关通常负责路由功能,用于整合各大微服务,而在网关之前一般使用nginx等并发能力强的服务抵御第一波用户请求的冲击。
同时,由于微服务网关担任请求分发的功能,一旦网关挂了,用户就无法正常使用了。故微服务网关必须集群,且可以根据不同的系统搭建不同的微服务网关。
根据实际情况可以有多种微服务,如:
二、微服务网关的搭建
2.1 创建工程
工程创建... 忽略
加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
创建配置文件
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
spring:
application:
name: gateway-web
redis:
host: 192.168.47.142
port: 6379
cloud:
gateway:
globalcors:
# 跨域请求配置
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
# 唯一标识符
- id: changgou_goods-route
# 用户请求需要路由到该服务[指定要路由的服务]
# uri: http://localhost:18081
uri: lb://goods
# 路由断言,路由规则配置
# lb 使用LoadBalancerClient 实现负载均衡,后面的goods是微服务的名称
predicates:
# 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
# - Host=cloud.tom.com**
# - Path=/api/brand/**
- Path=/**
filters:
# 将请求路径中的第一个路径去掉,请求路径以/区分
- StripPrefix=1
# - PrefixPath=/brand
- name: RequestRateLimiter #局部显示流过滤,名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 用户身份唯一识别标识符
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true
2.2 跨域和过滤配置
2.2.1 跨域路由配置
由于网关是给所有用户访问的,故没有nginx代理的情况下需要做跨域路由配置
globalcors:
# 跨域请求配置
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
2.2.2 Host配置
使用 host 配置可以让外部访问到网关
主要配置内容为:
# HOST 配置
routes:
# 唯一标识符
- id: changgou_goods-route
# 用户请求需要路由到该服务[指定要路由的服务]
uri: http://localhost:18081
# 路由断言,路由规则配置
predicates:
# 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
- Host=cloud.tom.com**
由于是在本机上测试,需要修改本地host
# 打开 hosts 文件
C:\Windows\System32\drivers\etc
# 添加内容
127.0.0.1 cloud.tom.com
测试:
http://cloud.tom.com:8001/brand
2.2.3 Path 路径过滤
Path 路径过滤与 host 配置不能共用
routes:
# 唯一标识符
- id: changgou_goods-route
# 用户请求需要路由到该服务[指定要路由的服务]
uri: http://localhost:18081
# 路由断言,路由规则配置
predicates:
# 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
# - Host=cloud.tom.com**
- Path=/brand/**
测试:
http://localhost:8001/brand
2.2.4 去掉请求前缀
通过 StripPrefix 可以指定去掉第几个前缀路径
routes:
# 唯一标识符
- id: changgou_goods-route
# 用户请求需要路由到该服务[指定要路由的服务]
uri: http://localhost:18081
# 路由断言,路由规则配置
predicates:
# 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
- Path=/api/brand/**
filters:
# 将请求路径中的第一个路径去掉,请求路径以/区分
- StripPrefix=1
测试:
http://localhost:8001/api/brand
2.2.5 增加前缀
主动给所有的请求增加前缀
routes:
# 唯一标识符
- id: changgou_goods-route
# 用户请求需要路由到该服务[指定要路由的服务]
uri: http://localhost:18081
# 路由断言,路由规则配置
predicates:
# 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
- Path=/**
filters:
# 将请求路径中的第一个路径去掉,请求路径以/区分
- PrefixPath=/brand
测试:
http://localhost:8001/
2.2.6 负载均衡配置
在并发量较大的时候,我们需要根据服务的名称判断来做负载均衡,可以使用 LoadBalancerClientFilter 实现负载均衡调用。
LoadBalancerClientFilter 会作用在 url 中以 lb 开头的路由,利用 loadBalancer 来获取服务实例,构造目标的 requestUrl,并设置到 GATEWAY_REQUEST_URL_ATTR 属性中供 NettyRoutingFilter 使用
修改uri
2.3 网关限流
2.3.1 限流介绍
Nginx 限流:
由于 Nginx 的访问中带有大量的静态资源访问和各大微服务的访问,故经过 Nginx 限流后请求数目可能依旧很大。故这时候可以通过网关限流的方式处理。内部实现的算法为漏桶算法。
网关限流:
网关限流可以对各个微服务设定不同的限流速率,防止由于大量并发导致服务崩溃。其内部是使用了 令牌桶算法 实现的。常见的实现令牌桶算法的技术有 Guaua、Redis。
模拟场景:
攻击者通过某个接口一直访问文件上传微服务,若 Nginx 的限流策略为 100/r,则文件上传服务可能同时面对 50/s 个并发请求。这时候可能导致文件服务难以支撑。这就要通过网关限流的方式,防止微服务负载过高。且通过约定好的识别方式,直接拦截住恶意请求。
2.3.2 作用
保护业务微服务,防止雪崩效应。
2.3.3 使用
引入依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
在网关的项目启动类增加方法:
/**
* @File: GatewayWebApplication
* @Description:
* @Author: tom
* @Create: 2020-06-10 11:36
**/
@SpringBootApplication
@EnableEurekaClient
@Slf4j
public class GatewayWebApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayWebApplication.class);
}
/**
* 创建用户唯一标识,使用 IP 作为用户的唯一标识,根据 IP 进行限流操作
* @return
*/
@Bean("ipKeyResolver")
public KeyResolver userKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just("需要使用的用户身份识别唯一标识");
String ip = exchange.getRequest().getRemoteAddress().getHostName();
log.info("用户请求的IP为:" + ip);
return Mono.just(ip);
}
};
}
}
修改配置文件:
spring:
application:
name: gateway-web
redis:
host: 192.168.47.142
port: 6379
cloud:
gateway:
globalcors:
# 跨域请求配置
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
# 唯一标识符
- id: changgou_goods-route
uri: lb://goods
- Path=/**
filters:
- StripPrefix=1
############## 配置令牌桶 ##############
# 默认 redis 为127.0.0.1:6379,需根据实际情况配置
- name: RequestRateLimiter #局部显示流过滤,名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 用户身份唯一识别标识符
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
使用JMeter 测试
三、微服务网关结合 JWT 实现用户登录校验
3.1 整体框架流程
- 用户通过用户中心网关访问用户微服务,进行登录
- 用户微服务利用 JWT 生成 token 令牌,并在 Header 中返回给用户
- 用户之后的请求到要带上这个 token 令牌,在请求到达用户中心网关
- 用户中心网关会对令牌的正确性和实效性进行验证,成功且有效则会继续向下分发,若令牌错误或过期则会拒绝请求
3.2 使用
3.2.1 加入工具类 JwtUtil.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具类
*/
public class JwtUtil {
// 有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
// 设置秘钥明文
public static final String JWT_KEY = "tomcast";
// 设置颁发者
public static final String JWT_ISS = "tom";
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer(JWT_ISS) // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);// 设置过期时间
return builder.compact();
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析令牌数据
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
}
}
3.2.2 增加 login 接口
/**
* 用户登录
* @param username
* @param password
* @return
*/
@GetMapping("/login")
public Result login(String username, String password, HttpServletResponse response) {
// 查询用户信息
User user = userService.findById(username);
// 对比密码
if (BCrypt.checkpw(password, user.getPassword())) {
// 创建用户令牌信息
Map<String, Object> tokenMap = new HashMap<String, Object>();
tokenMap.put("role","USER");
tokenMap.put("success", "SUCCESS");
tokenMap.put("username", username);
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenMap), null);
// 将令牌信息存入到Cookie
Cookie cookie = new Cookie("Authorization", token);
cookie.setDomain("localhost");
cookie.setPath("/");
response.addCookie(cookie);
// 将令牌作为参数给用户
return Result.ok("登录成功", token);
}
// 密码匹配失败,登录失败
return Result.error("账号或密码有误");
}
3.2.3 修改网关服务代码
配置启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @File: GatewayWebApplication
* @Description:
* @Author: tom
**/
@SpringBootApplication
@EnableEurekaClient
public class GatewayWebApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayWebApplication.class);
}
/**
* 创建用户唯一标识,使用 IP 作为用户的唯一标识,根据 IP 进行限流操作
* @return
*/
@Bean("ipKeyResolver")
public KeyResolver userKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just("需要使用的用户身份识别唯一标识");
String ip = exchange.getRequest().getRemoteAddress().getHostName();
return Mono.just(ip);
}
};
}
}
配置过滤器 AuthorizeFilter
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @File: AuthorizeFilter
* @Author: tom
* @Create: 2020-06-11 13:57
* @Description: 全局过滤器,实现用户权限鉴别(校验)
**/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "Authorization";
/**
* 全局拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 获取用户令牌信息
// 1. 从 Header 中获取
String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
Boolean isInHeader = true;
// 2. 从 参数 中获取
if (StringUtils.isEmpty(token)) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
isInHeader = false;
}
// 3. 从 Cookie 中获取
if (StringUtils.isEmpty(token)) {
HttpCookie httpCookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
if(httpCookie != null) {
token = httpCookie.getValue();
isInHeader = false;
}
}
// 若没有令牌,则拦截
if (StringUtils.isEmpty(token)) {
// 传入空数据
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 若有令牌,则校验令牌是否有效
try {
// 解析成功,则放行
JwtUtil.parseJWT(token);
} catch (Exception e) {
// 无效则拦截
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 将令牌封装到头文件中
if (isInHeader) {
request.mutate().header(AUTHORIZE_TOKEN, token);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}