Spring Cloud Gateway
1基础
1.1理论说明
网关旨在为微服务架构提供- 种简单而有效的统-的API路由管理方式。
●在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。
●存在的问题:
● 客户端多次请求不同的微服务,增加客户端的复杂性
●认证复杂,每个服务都要进行认证
●http请求不同服务次数增加, 性能不高
使用Gateway之后,用户之和网关打交道,不和服务打交道。解决了上面三个问题
●网关就是系统的入口,封装了应用程序的内部结构,为客户端提供统-服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等
●在目前的网关解决方案里,有Nginx+ Lua、 Netlix Zuul、Spring Cloud Gateway等等
1.2快速入门
1.2.1 首先在原来项目中创建一个服务 ,名为Gateway;创建之后先只设置启动类和配置文件
1.2.2在pom.xml中引入相关依赖
<dependencies>
<!-- 引入Gateway相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 因为后续要和eureka相关操作,此处引用eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
1.2.3编写配置文件
在Gateway服务的配置文件(application.yml)中(静态路由)
server:
port: 8099
spring:
application:
name: backend-fateway
cloud:
gateway:
routes:
#添加第一个服务
- id: backend-show-provider-fateway
#静态路由
uri: http://localhost:7010/
#配置访问地址规则,provider 是show-provider服务中的一个控制器
predicates:
- Path=/provider/**
discovery:
locator:
enabled: true #开启微服务发现功能
lower-case-service-id: true # 请求上服务名配置为小写(因为erueka 默认为大写)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# 访问 http://localhost:8099/provider/sayhello2?message=2233 就会有 http://localhost:7010/provider/sayhello2?message=2233结果
配置好之后,通过访问【Gateway服务】的ip+【show-provider服务】的地址就能得到 正常访问【show-provider服务】的结果
配置静态访问就有一个问题,如果不同服务下有相同的Controller名称和 方法名;所以就需要考虑使用动态路由
server:
port: 8099
spring:
application:
name: backend-fateway
cloud:
gateway:
routes:
#添加第一个服务
- id: backend-show-provider-fateway
#静态路由
# uri: http://localhost:7010/
#动态路由
uri: lb://backend-show-provider
predicates:
- Path=/provider/**
discovery:
locator:
enabled: true #开启微服务发现功能
lower-case-service-id: true # 请求上服务名配置为小写(因为erueka 默认为大写)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# 访问 http://localhost:8099/provider/sayhello2?message=2233 就会有 http://localhost:7010/provider/sayhello2?message=2233结果
1.3Gateway过滤器
1.3.1基础
●Gateway 支持过滤器功能,对请求或响应进行拦截,完成一些通用操作。
●Gateway 提供两种过滤器方式: "pre" 和"post"
●pre过滤器,在转发之前执行,可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
●post过滤器,在响应之前执行,可以做响应内容、响应头的修改,日志的输出,流量监控等。
●Gateway还提供了两种类型过滤器
·GatewayFilter:局部过滤器,针对单个路由
·GlobalFilter :全局过滤器,针对所有路由
1.3.2局部过滤器
- GatewayFilter局部过滤器,是针对单个路由的过滤器。
- 在Spring Cloud Gateway组件中提供了大量内置的局部过滤器,对请求和响应做过滤操作。
- 遵循约定大于配置的思想,只需要在配置文件配置局部过滤器名称,并为其指定对应的值,就可以让其生效。
配置工厂:https://blog.csdn.net/abu935009066/article/details/112252692
1.3.3全局过滤器
GlobalFilter全局过滤器,不需要在配置文件中配置,系统初始化时加载,并作用在每个路由上。
Spring Cloud Gateway核心的功能也是通过内置的全局过滤器来完成
自定义全局过滤器步骤:
1.定义类实现GlobalFilter和Ordered接口
package com.item.bakend.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class MyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// return null;
System.out.println("全局过滤器");
return chain.filter(exchange);//放行
}
/**
* 过滤器排序,值越小-越先执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
2.复写方法
3.完成逻辑处理
2.Zuul
2.1基础
◆ Zuul是网关大军中的一员,目前市场使用规律比较高,
◆ Zuu|除了实现请求转发和过滤,一般还作为鉴权和容错使用
◆Zuul可以无缝衔接Ribbon和Hystrix
◆使用Zuul也是实现Gateway,请求方式 :
格式: http://localhost:Zuul服务的端口/配置的被访问服务名称/Controller名/方法名
demo: http://localhost:8080/film-api/film/actors
请求路由
◆Zuul可以通过配置完成请求路由配置
◆Zuul服务路由默认支持serviceld作为上下文
◆ignored-services可以禁用serviceld
demo:被请求服务backend_film。
创建一个zuul服务,主要就是 myFilter.java和application.yml文件。
application.yml中配置zuul相关信息
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
backend_film: #application.name 服务名称 请求方式1(不推荐-暴露服务名称 )=》http://localhost:8080/backend_film/film/actors =》http://localhost:8080/backend_film +Controller 名称+方法名称 和下面效果一样
path: /film-api/** #匹配规则 请求路劲以 film-api开头 ,后面不管是什么,都会去请求backend_film 的内容 例如 http://localhost:8080/film-api/film/actors :http://localhost:8080/film-api +Controller 名称+方法名称
logging:
config: classpath:logback.xml
MyFilter.java配置过滤信息.一般可以做jwt判断等等
package com.item.backed.zuul.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@Component
public class MyFilter extends ZuulFilter {
/**
* @Description: Filter类型
* @Param: []
* @return: java.lang.String
* @Author: jiangzh
*/
@Override
public String filterType() {
return "pre";
}
/**
* @Description: filter的执行顺序
* @Param: []
* @return: int
* @Author: jiangzh
*/
@Override
public int filterOrder() {
return 0;
}
/**
* @Description: 是否要拦截
* @Param: []
* @return: boolean
* @Author: jiangzh
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* @Description: Filter的具体业务逻辑
* @Param: []
* @return: java.lang.Object
* @Author: jiangzh
*/
@Override
public Object run() throws ZuulException { //编写顾虑器拦截业务逻辑代码
// 案例:拦截所有都服务接口,判断服务接口上是否有传递userToekn参数
//获取上下文
RequestContext currentContext = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = currentContext.getRequest();
//验证token时候 token的参数 从请求头获取
String userToken = request.getParameter("userToken");
if (StringUtils.isEmpty(userToken)) {
//返回错误提示
currentContext.setSendZuulResponse(false); //false 不会继续往下执行 不会调用服务接口了 网关直接响应给客户了
currentContext.setResponseBody("you have not userToken");
currentContext.setResponseStatusCode(401);
return null;
}
//否则正常执行 调用服务接口...
return null;
}
// public Object run() throws ZuulException {
// // ThreadLocal
// RequestContext requestContext = RequestContext.getCurrentContext();
//
// HttpServletRequest request = requestContext.getRequest();
//
// Enumeration<String> headerNames = request.getHeaderNames();
// while (headerNames.hasMoreElements()){
// String headName = headerNames.nextElement();
// log.info("headName:{}, headValue:{}", headName, request.getHeader(headName));
// }
//
// return null;
// }
}
application.yml的请求路由表达式
◆? ->匹配任意单个字符
◆* ->配置任意数量的字符
◆** ->配置任意数量的字符,支持多级目录.
补充---zuul prefix
#zuul:
# routes:
# backend_film: #application.name 服务名称 请求方式1(不推荐-暴露服务名称 )=》http://localhost:8080/backend_film/film/actors =》http://localhost:8080/backend_film +Controller 名称+方法名称 和下面效果一样
# path: /film-api/** #匹配规则 请求路劲以 film-api开头 ,后面不管是什么,都会去请求backend_film 的内容 例如 http://localhost:8080/film-api/film/actors :http://localhost:8080/film-api +Controller 名称+方法名称
zuul:
prefix: "/meetingfilm/" ## 统一前缀
routes:
meetingfilm-user:
path: /userapi/** #匹配规则
serviceId: backend_user #服务名称
retryable: true # 是否允许重试 , 饿汉模式
backend_film:
path: /filmapi/** #匹配规则 例如 http://localhost:8080/meetingfilm/filmapi/film/actors
serviceId: backend_film #服务名称
retryable: true
2.2Zuul整合其他
2.2.1整合Hystrix
application.yml配置
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
backend_film: #application.name 服务名称 请求方式1(不推荐-暴露服务名称 )=》http://localhost:8080/backend_film/film/actors =》http://localhost:8080/backend_film +Controller 名称+方法名称 和下面效果一样
path: /film-api/** #匹配规则 请求路劲以 film-api开头 ,后面不管是什么,都会去请求backend_film 的内容 例如 http://localhost:8080/film-api/film/actors :http://localhost:8080/film-api +Controller 名称+方法名称
logging:
config: classpath:logback.xml
# hystrix配置
hystrix:
command:
default:
excution:
timeout:
enable: true #开启超时监控
isolation:
thread:
timeoutInMilliseconds: 10 #超时监控 毫秒
然后添加一个class文件,继承FallbackProvider,同时注意使用 @Component 注解
package com.item.backed.zuul.fallbacks;
import com.alibaba.fastjson.JSONObject;
import com.item.backed.utils.common.vo.BaseResponseVO;
import com.item.backed.utils.exception.CommonServiceException;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 降级服务处理
*/
@Component
public class MyFallback implements FallbackProvider {
/**
* 针对哪一个路由进行降级, return可以写 *;backend_film是一个服务名,此处代表针对zuul中所有针对backend_film的请求都有熔断监控
* @return
*/
@Override
public String getRoute() {
return "backend_film";
}
/**
* 降级时处理方式
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
/**
* @Description: 业务降级处理方式
* 熔断之后 返回 404, "No backend_film!~"
*/
@Override
public InputStream getBody() throws IOException {
BaseResponseVO responseVO
= BaseResponseVO.serviceException(
new CommonServiceException(404, "No backend_film!~"));
String result = JSONObject.toJSONString(responseVO);
return new ByteArrayInputStream(result.getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
微服务网关Zuul路由ZuulFilter过滤,创建一个class文件 继承ZuulFilter,然后在其中可以进行请求参数的获取以及拦截
package com.item.backed.zuul.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Slf4j
@Component
public class MyFilter extends ZuulFilter {
/**
* @Description: Filter类型
* @Param: []
* @return: java.lang.String
* @Author: jiangzh
*/
@Override
public String filterType() {
return "pre";
}
/**
* @Description: filter的执行顺序
* @Param: []
* @return: int
* @Author: jiangzh
*/
@Override
public int filterOrder() {
return 0;
}
/**
* @Description: 是否要拦截
* @Param: []
* @return: boolean
* @Author: jiangzh
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* @Description: Filter的具体业务逻辑
* @Param: []
* @return: java.lang.Object
* @Author: jiangzh
*/
@Override
public Object run() throws ZuulException {
// ThreadLocal
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String userToken = request.getHeader("userToken");
if(StringUtils.isEmpty(userToken)){
//返回错误提示
RequestContext currentContext = RequestContext.getCurrentContext();
currentContext.setSendZuulResponse(false); //false 不会继续往下执行 不会调用服务接口了 网关直接响应给客户了
currentContext.setResponseBody("you have not userToken");
currentContext.setResponseStatusCode(401);
return null;
}
String cookie = request.getHeader("Cookie");
String header = request.getHeader("Set-Cookie");
log.info("Object userToken:{}",userToken);
log.info("Object cookie:{}",cookie);
log.info("Object header:{}",header);
//打印所有的头部参数 key、value
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()){
String headName = headerNames.nextElement();
//获取ing求头
log.error("Object headName:{}, headValue:{}", headName, request.getHeader(headName));
}
return null;
}
}
后台日志打印
通过ZuulFilter 完成jwt以及 Cors跨域
Cors解决跨域问题
package com.item.backed.zuul.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* @description : 解决跨域问题
**/
@Component
public class CorsFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 跨域
HttpServletResponse response = ctx.getResponse();
response.addHeader("Access-Control-Allow-Origin", "*");//运行的请求来源 =》 *代表所有,一般生产环境会指定某一个请求网址 比如www.tm.com
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE,PUT");//OPTIONS一定要有
response.setHeader("Access-Control-Allow-Headers","DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization");
response.setContentType("application/json");//返回格式
response.setCharacterEncoding("UTF-8");
// response.setContentType("text/html;charset=UTF-8");
/*
跨域资源共享
- 这是HTTP协议规定的安全策略
- 配置资源共享的方式和目标方
前端: node+vue -> admin.meetingfilm.com
后端: springboot -> backend.meetingfilm.com
-> 示例
缺陷:如果出现跨域策略不足的情况,需要修改代码,重新部署
-> Nginx
*/
return null;
}
}
jwt验证
package com.item.backed.utils.properties;
/**
* jwt基础文件
*/
public class JwtProperties {
private static com.item.backed.utils.properties.JwtProperties jwtProperties = new com.item.backed.utils.properties.JwtProperties();
private JwtProperties(){}
public static com.item.backed.utils.properties.JwtProperties getJwtProperties(){
return jwtProperties;
}
public static final String JWT_PREFIX = "jwt";
private String header = "Authorization";
private String secret = "defaultSecret";
private Long expiration = 604800L; // 默认是七天
private String authPath = "login";
private String md5Key = "randomKey";
public static String getJwtPrefix() {
return JWT_PREFIX;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpiration() {
return expiration;
}
public void setExpiration(Long expiration) {
this.expiration = expiration;
}
public String getAuthPath() {
return authPath;
}
public void setAuthPath(String authPath) {
this.authPath = authPath;
}
public String getMd5Key() {
return md5Key;
}
public void setMd5Key(String md5Key) {
this.md5Key = md5Key;
}
}
jwt ZuulFile文件
package com.item.backed.zuul.filters;
import com.alibaba.fastjson.JSONObject;
import com.item.backed.utils.common.vo.BaseResponseVO;
import com.item.backed.utils.properties.JwtProperties;
import com.item.backed.utils.util.JwtTokenUtilx;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @description : jwt过滤
**/
@Slf4j
@Component
public class JWTFilter extends ZuulFilter {
/**
* @Description: Filter类型
* @Param: []
* @return: java.lang.String
* @Author: jiangzh
*/
@Override
public String filterType() {
return "pre";
}
/**
* @Description: filter的执行顺序
* @Param: []
* @return: int
* @Author: jiangzh
*/
@Override
public int filterOrder() {
return 0;
}
/**
* @Description: 是否要拦截
* @Param: []
* @return: boolean
* @Author: jiangzh
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* @Description: Filter的具体业务逻辑
* @Param: []
* @return: java.lang.Object
* @Author: jiangzh
*/
@Override
public Object run() throws ZuulException {
// JWT工具类
JwtTokenUtilx jwtTokenUtil = new JwtTokenUtilx();
JwtProperties jwtProperties = JwtProperties.getJwtProperties();
// ThreadLocal
RequestContext ctx = RequestContext.getCurrentContext();
// 获取当前请求和返回值
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
// 提前设置请求继续,如果失败则修改此内容
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
String pth = "/" + jwtProperties.getAuthPath();
// 判断是否是登陆,如果是登陆则不验证JWT
if (request.getServletPath().endsWith("/" + jwtProperties.getAuthPath())) {
return null;
}
// 1、验证Token有效性 -> 用户是否登录过
final String requestHeader = request.getHeader(jwtProperties.getHeader());
String authToken = null;
// Bearer header.payload.sign
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
//验证token是否过期,包含了验证jwt是否正确
try {
boolean flag = jwtTokenUtil.isTokenExpired(authToken);
if (flag) {
renderJson(ctx, response, BaseResponseVO.noLogin());
} else {
// 2、解析出JWT中的payload -> userid - randomkey
String randomkey = jwtTokenUtil.getMd5KeyFromToken(authToken);
String userId = jwtTokenUtil.getUsernameFromToken(authToken);
// 3、是否需要验签,以及验签的算法
// 4、判断userid是否有效
// TODO
}
} catch (JwtException e) {
//有异常就是token解析失败
renderJson(ctx, response, BaseResponseVO.noLogin());
}
} else {
//header没有带Bearer字段
renderJson(ctx, response, BaseResponseVO.noLogin());
}
return null;
}
/**
* 渲染json对象
*/
public static void renderJson(RequestContext ctx, HttpServletResponse response, Object jsonObject) {
// 设置终止请求
response.setHeader("Content-Type", "application/json;charset=UTF-8");
ctx.setSendZuulResponse(false);
ctx.setResponseBody(JSONObject.toJSONString(jsonObject));
}
}
前端请求