feat(master): SpringCLoud Gateway

Gateway 相关的更新
master
土豆兄弟 1 month ago
parent eed300274c
commit 400a561234

@ -36,6 +36,35 @@
---
## 5. 解读 SpringCloud Gateway Filter
- SpringCloud Gateway Filter 的相关概念
- SpringCloud Gateway 基于过滤器实现,同 zuul 类似,有 pre 和 post 两种方式的filter分别处理前置逻辑和后置逻辑
- 客户端的请求先经过 pre 类型的 filter然后将请求转发到具体的业务服务收到业务服务的响应之后再经过 post 类型的 fiter 处理,最后返回响应到客户端
- Filter 一共有两大类: 全局过滤器 和 局部过滤器
- 搜索 GlobalFilter 就可以知道 Gateway 所支持的所有过滤器
- 添加前缀的局部过滤器: PrefixPathGatewayFilterFactory -> 局部过滤器返回均为 GatewayFilter
- 添加后缀过滤器(去掉前缀): StripPrefixGatewayFilterFactory
---
- SpringCloud Gateway Filter 的执行流程
- 过滤器有优先级之分Order越大优先级越低越晚被执行
- 全局过滤器所有的请求都会执行
- 局部过滤器只有配置的请求才会执行
---
## 6. 局部过滤器 - 校验 Header 中的 Token
- 实现一个局部过滤器, 和局部过滤器工厂
- [HeaderTokenGatewayFilter.java] + [HeaderTokenGatewayFilterFactory.java]
---
## 7. 缓存 HTTP 请求 Body 的全局过滤器
- 缓存 HTTP 请求 Body 的全局过滤器
- [GlobalCacheRequestBodyFilter.java]
---
## 8. 登录、注册、鉴权全局过滤器
- [GlobalLoginOrRegisterFilter.java]
---
## 9. 代码与文件两种方式配置网关路由
## 10. 验证网关微服务功能可用性
## 11. SpringCloud Gateway 微服务入口网关总结

@ -18,6 +18,7 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jjwt.version>0.9.1</jjwt.version>
</properties>
<!-- 模块名及描述信息 -->
@ -62,11 +63,20 @@
<!-- <artifactId>e-commerce-common</artifactId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
<!--

@ -0,0 +1,19 @@
package org.example.common.constant;
/**
* <h1></h1>
* */
public final class CommonConstant {
/** RSA 公钥 */
public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnV+iGlE1e8Z825G+ChIwRJ2H2jOMCBu" +
"HV7BPrUE8dAGjqAlRtCaxMyJw7NV9NIUl/rY7RWBUQwelkGmGuQomnUAFIgN9f8UxSC6G935lo1ZoBVJWYmfs5ToXLz+fQugmqHZvF+Vc5l" +
"UEo1YapeiaymkOxDORMGjzQBoxoBt316IAwNEPIvcV+F6T+WNFJX/p5Xj48Z1rtmbOQ8ffF+pEWKZGsYg/9b+pKiqFJtuyHqwj/9oxFBE98" +
"MCu5RfK6M7Ff9/1dyNen1HKjI7Awj8ZnSceVUldcXEdnP89YagevbhtSl/+CvCsKwHq5+ZLkcuONSxE4dIFWTjxA92wJjYf9wIDAQAB";
/** JWT 中存储用户信息的 key */
public static final String JWT_USER_INFO_KEY = "e-commerce-user";
/** 授权中心的 service-id */
public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authority-center";
}

@ -0,0 +1,63 @@
package org.example.common.util;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import org.example.common.constant.CommonConstant;
import org.example.common.vo.LoginUserInfo;
import sun.misc.BASE64Decoder;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Calendar;
/**
* <h1>JWT Token </h1>
* */
public class TokenParseUtil {
/**
* <h2> JWT Token LoginUserInfo </h2>
* */
public static LoginUserInfo parseUserInfoFromToken(String token) throws Exception {
if (null == token) {
return null;
}
Jws<Claims> claimsJws = parseToken(token, getPublicKey());
Claims body = claimsJws.getBody();
// 如果 Token 已经过期了, 返回 null
if (body.getExpiration().before(Calendar.getInstance().getTime())) {
return null;
}
// 返回 Token 中保存的用户信息
return JSON.parseObject(
body.get(CommonConstant.JWT_USER_INFO_KEY).toString(),
LoginUserInfo.class
);
}
/**
* <h2> JWT Token</h2>
* */
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
/**
* <h2> PublicKey </h2>
* */
private static PublicKey getPublicKey() throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
}

@ -0,0 +1,36 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* <h1></h1>
* {
* "code": 0,
* "message": "",
* "data": {}
* }
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> implements Serializable {
/** 错误码 */
private Integer code;
/** 错误消息 */
private String message;
/** 泛型响应数据 */
private T Data;
public CommonResponse(Integer code, String message) {
this.code = code;
this.message = message;
}
}

@ -0,0 +1,17 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1> Token</h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtToken {
/** JWT */
private String token;
}

@ -0,0 +1,20 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1></h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserInfo {
/** 用户 id */
private Long id;
/** 用户名 */
private String username;
}

@ -0,0 +1,20 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1></h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsernameAndPassword {
/** 用户名 */
private String username;
/** 密码 */
private String password;
}

@ -0,0 +1,17 @@
package org.example.conf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* <h1> Bean</h1>
* */
@Configuration
public class GatewayBeanConf {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

@ -0,0 +1,21 @@
package org.example.constant;
/**
* <h1></h1>
* */
public class GatewayConstant {
/** 登录 uri */
public static final String LOGIN_URI = "/dev-protocol-springcloud-gateway/login";
/** 注册 uri */
public static final String REGISTER_URI = "/dev-protocol-springcloud-gateway/register";
/** 去授权中心拿到登录 token 的 uri 格式化接口 */
public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
"http://%s:%s/dev-protocol-authority-center/authority/token";
/** 去授权中心注册并拿到 token 的 uri 格式化接口 */
public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
"http://%s:%s/dev-protocol-authority-center/authority/register";
}

@ -0,0 +1,63 @@
package org.example.filter;
import lombok.extern.slf4j.Slf4j;
import org.example.constant.GatewayConstant;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* <h1> body </h1>
* Spring WebFlux
* */
@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 只对 注册和登录的信息进行缓存
boolean isloginOrRegister =
exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
|| exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);
if (null == exchange.getRequest().getHeaders().getContentType()
|| !isloginOrRegister) {
return chain.filter(exchange);
}
// DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
// 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
DataBufferUtils.retain(dataBuffer);
// defer、just 都是去创建数据源, 得到当前数据的副本
Flux<DataBuffer> cachedFlux = Flux.defer(() ->
Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
// 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
ServerHttpRequest mutatedRequest =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
// 将包装之后的 ServerHttpRequest 向下继续传递
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 1;
}
}

@ -0,0 +1,40 @@
package org.example.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
/**
* <h1></h1>
* */
@Slf4j
@Component
public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 前置逻辑
StopWatch sw = StopWatch.createStarted();
String uri = exchange.getRequest().getURI().getPath();
return chain.filter(exchange).then(
// 后置逻辑
Mono.fromRunnable(() ->
log.info("[{}] elapsed: [{}ms]",
uri, sw.getTime(TimeUnit.MILLISECONDS)))
);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
}

@ -0,0 +1,175 @@
package org.example.filter;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.example.common.constant.CommonConstant;
import org.example.common.util.TokenParseUtil;
import org.example.common.vo.JwtToken;
import org.example.common.vo.LoginUserInfo;
import org.example.common.vo.UsernameAndPassword;
import org.example.constant.GatewayConstant;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* <h1></h1>
* */
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
/** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
private final LoadBalancerClient loadBalancerClient;
private final RestTemplate restTemplate;
public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
RestTemplate restTemplate) {
this.loadBalancerClient = loadBalancerClient;
this.restTemplate = restTemplate;
}
/**
* <h2></h2>
* 1. , Token
* 2. 访, , 401
* */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 1. 如果是登录
if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
// 去授权中心拿 token
String token = getTokenFromAuthorityCenter(
request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
);
// header 中不能设置 null
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
// 2. 如果是注册
if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
// 去授权中心拿 token: 先创建用户, 再返回 Token
String token = getTokenFromAuthorityCenter(
request,
GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
);
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
// 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
LoginUserInfo loginUserInfo = null;
try {
loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
} catch (Exception ex) {
log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
}
// 获取不到登录用户信息, 返回 401
if (null == loginUserInfo) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 解析通过, 则放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
/**
* <h2> Token</h2>
* */
private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
// service id 就是服务名字, 负载均衡
ServiceInstance serviceInstance = loadBalancerClient.choose(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("Nacos Client Info: [{}], [{}], [{}]",
serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
JSON.toJSONString(serviceInstance.getMetadata()));
String requestUrl = String.format(
uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
);
UsernameAndPassword requestBody = JSON.parseObject(
parseBodyFromRequest(request), UsernameAndPassword.class
);
log.info("login request url and body: [{}], [{}]", requestUrl, JSON.toJSONString(requestBody));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JwtToken token = restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(requestBody), headers),
JwtToken.class
);
if (null != token) {
return token.getToken();
}
return null;
}
/**
* <h2> Post </h2>
* */
private String parseBodyFromRequest(ServerHttpRequest request) {
// 获取请求体
Flux<DataBuffer> body = request.getBody();
// 原子引用
AtomicReference<String> bodyRef = new AtomicReference<>();
// 订阅缓冲区去消费请求体中的数据
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
// 一定要使用 DataBufferUtils.release 释放掉, 因为前面使用 DataBufferUtils.retain(dataBuffer); 保证不被释放, 这里不手动释放, 会出现内存泄露
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
// 获取 request body
return bodyRef.get();
}
}

@ -0,0 +1,32 @@
package org.example.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* <h1>HTTP Token </h1>
* */
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从 HTTP Header 中寻找 key 为 token, value 为 imooc 的键值对
String name = exchange.getRequest().getHeaders().getFirst("token");
if ("imooc".equals(name)) {
return chain.filter(exchange);
}
// 标记此次请求没有权限, 并结束这次请求
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
}

@ -0,0 +1,16 @@
package org.example.filter.factory;
import org.example.filter.HeaderTokenGatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeaderTokenGatewayFilter();
}
}
Loading…
Cancel
Save