1:实现gateway动态路由
配置文件
server:
port: 9001
servlet:
context-path: /imooc
spring:
application:
name: e-commerce-gateway
cloud:
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 192.168.5.253:8848 # Nacos 服务器地址
namespace: d8fa475f-a1df-4cd3-acbf-e9d6df0ca59f
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 静态路由,本项目使用的是动态路由
# gateway:
# routes:
# - id: path_route # 路由的ID
# uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
# predicates: # 断言, 路径相匹配的进行路由
# - Path=/user/{id}
kafka:
bootstrap-servers: 192.168.5.253:9092
producer:
retries: 3
consumer:
auto-offset-reset: latest
# zipkin:
# sender:
# type: kafka # 默认是 web
# base-url: http://localhost:9411/
main:
allow-bean-definition-overriding: true # 因为将来会引入很多依赖, 难免有重名的 bean
# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:
gateway:
route:
config:
data-id: e-commerce-gateway-router
group: e-commerce
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
gateway配置类
/**
* ProjectName xyc-commerce-springcloud
* 读取nacos相关的配置项,用户配置监听器
* @author xieyucan
* <br>CreateDate 2022/9/5 13:50
*/
@Configuration
public class GatewayConfig {
/**
* 获取读取超时时间
*/
public static final long DEFAULT_TIMEOUT=30000;
/**
* 服务器地址
*/
public static String NACOS_SERVER_ADDR;
/**
* 命名空间
*/
public static String NACOS_NAMESPACE;
/**
* data_id
*/
public static String NACOS_ROUTE_DATA_ID;
/**
* nacos分组id
*/
public static String NACOS_GROUP_ID;
/**
* 这里的意思是,将@Value("${spring.cloud.nacos.discovery.server-addr}") 赋值给nacosServerAddr
* 然后 NACOS_SERVER_ADDR=nacosServerAddr;
* @param nacosServerAddr
*/
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr)
{
NACOS_SERVER_ADDR=nacosServerAddr;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNacosNamespace(String nacosNamespace)
{
NACOS_NAMESPACE=nacosNamespace;
}
@Value("${nacos.gateway.route.config.group}")
public void setNacosGroupId(String groupId)
{
NACOS_GROUP_ID=groupId;
}
@Value("${nacos.gateway.route.config.data-id}")
public void setNacosRouteDataId(String dataId)
{
NACOS_ROUTE_DATA_ID=dataId;
}
}
实现事件推送aware:动态更新路由网关 Service
/**
* ProjectName xyc-commerce-springcloud
* 实现事件推送aware:动态更新路由网关 Service
* @author xieyucan
* <br>CreateDate 2022/9/5 14:05
*/
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
/**
* 写路由定义 (gateway提供的bean)
*/
private final RouteDefinitionWriter routeDefinitionWriter;
/**
* 获取路由定义 (gateway提供的bean)
*/
private final RouteDefinitionLocator routeDefinitionLocator;
/**
* 事件发布对象
*/
private ApplicationEventPublisher publisher;
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
RouteDefinitionLocator routeDefinitionLocator) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeDefinitionLocator = routeDefinitionLocator;
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
//完成事件推送句柄初始化
this.publisher=applicationEventPublisher;
}
/**
* 增加路由定义
* @param routeDefinition
* @return
*/
public String addRouteDefinition(RouteDefinition routeDefinition)
{
log.info("gateway add route:[{}]",routeDefinition);
//保存路由配置并且发布出去
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
//发布事件通知给gateway,同步新增的路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 更新多个路由配置
* @param definitionList
* @return
*/
public String updateList(List<RouteDefinition> definitionList)
{
log.info("gateway update route:[{}]",definitionList);
//拿到当前gateway中存储的定义
List<RouteDefinition> routeDefinitionsExits =
routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
if(!CollectionUtil.isEmpty(routeDefinitionsExits))
{
//清除掉之前所有旧的路由定义
routeDefinitionsExits.forEach(rd->{
log.info("delete route definition:[{}]",rd);
deleteById(rd.getId());
});
}
//把更新的路由定义同步到gateway中
definitionList.forEach(definition->updateByRouteDefinition(definition));
return "success";
}
/**
* 根据路由id去删除路由配置
* @param id
* @return
*/
private String deleteById(String id)
{
try {
log.info("gateway delete route id:[{}]",id);
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "delete success";
}catch (Exception ex){
log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
return "delete fail";
}
}
/**
* 更新路由
* @param routeDefinition
* 更新的实现策略:删除+新增=更新
* @return
*/
private String updateByRouteDefinition(RouteDefinition routeDefinition)
{
try {
log.info("gateway update route:[{}]",routeDefinition);
this.routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()));
}catch (Exception ex){
return "update fail,not find routed"+routeDefinition.getId();
}
try {
this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}catch (Exception ex){
return "update route fail";
}
}
}
通过nacos下发动态路由配置,监听nacos中路由配置变更
/**
* ProjectName xyc-commerce-springcloud
* 通过nacos下发动态路由配置,监听nacos中路由配置变更
* @author xieyucan
* <br>CreateDate 2022/9/5 15:02
*/
@Slf4j
@Component
@DependsOn({"gatewayConfig"})
public class DynamicRouteServiceImplByNacos {
/**
* nacos 配置服务
*/
private ConfigService configService;
private final DynamicRouteServiceImpl dynamicRouteService;
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
this.dynamicRouteService = dynamicRouteService;
}
/**
* bean 在容器中构造完成之后会执行init方法
*/
@PostConstruct
public void init()
{
log.info("gateway route init");
try {
//初始化nacos配置客户端
configService=initConfigService();
if(configService==null)
{
log.error("init config service fail");
return;
}
//通过nacos config并且知道路由配置路径去获取路由配置
String configInfo=configService.getConfig(GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_GROUP_ID,
GatewayConfig.DEFAULT_TIMEOUT);
log.info("get current gateway config:[{}]",configInfo);
List<RouteDefinition> routeDefinitions =
JSON.parseArray(configInfo, RouteDefinition.class);
if(CollectionUtil.isNotEmpty(routeDefinitions))
{
for(RouteDefinition routeDefinition:routeDefinitions)
{
log.info("init gateway config:[{}]",routeDefinition.toString());
dynamicRouteService.addRouteDefinition(routeDefinition);
}
}
}catch (Exception e){
log.error("gateway route init has some error:[{}]",e.getMessage(),e);
}
//添加一个监听器
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_GROUP_ID);
}
/**
* 初始化nacosconfig,初始化之后才可以使用
* @return
*/
private ConfigService initConfigService()
{
try {
Properties properties = new Properties();
properties.setProperty("serverAddr",GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace",GatewayConfig.NACOS_NAMESPACE);
return configService= NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("init gateway nacos config error:[{}]",e.getMessage(),e);
return null;
}
}
/**
* 实现对nacos的监听,nacos下发的动态路由信息
* @param dataId
* @param group
*/
private void dynamicRouteByNacosListener(String dataId,String group)
{
try {
//给nacos config 客户端增加一个监听器
configService.addListener(dataId, group, new Listener() {
/**
* 提供线程池执行操作 return null;表示默认线程池
* @return
*/
@Override
public Executor getExecutor() {
return null;
}
/**
* 监听器受到配置中心的配置更新
* nacos 中最新的配置定义
* @param configInfo 配置本来就是String类型,所以,我们需要将其转换为数组对象
*/
@Override
public void receiveConfigInfo(String configInfo) {
log.info("start to update config:[{}]",configInfo);
List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo,
RouteDefinition.class);
log.info("update route:[{}]",routeDefinitions.toString());
dynamicRouteService.updateList(routeDefinitions);
}
});
}catch (NacosException ex){
log.error("dynamic update gateway config error:[{}]",ex.getErrCode(),ex);
}
}
}
2:gateway路由配置(局部过滤器)
[
{
"id": "e-commerce-nacos-client",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-nacos-client/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-nacos-client",
//局部过滤器的用法,也就是需要在网关中声明
"filters": [
{
"name": "HeaderToken"
},
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
},
{
"id": "e-commerce-account-service",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-account-service/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-account-service",
"filters": [
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
},
{
"id": "e-commerce-goods-service",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-goods-service/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-goods-service",
"filters": [
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
}
]
3:全局过滤器和局部过滤器
局部过滤器的使用(需要在网关配置类中声明,详细看第二点)
局部过滤器
/**
* ProjectName xyc-commerce-springcloud
* 请求头部携带token验证过滤器
* @author xieyucan
* <br>CreateDate 2022/9/6 9:08
*/
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//从http header中寻找key为token,value为imooc的键值对 (这里采用两个token,第一个为假的伪造token)
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;
}
}
局部过滤器工厂
/**
* ProjectName xyc-commerce-springcloud
* 让局部过滤器生效
* 实现局部过滤器步骤:
* 1:局部过滤器要实现 GatewayFilter, Ordered 相应方法
* 2:实现抽象过滤器工厂
* 3:在配置文件中配置 写GatewayFilterFactory 前面的即可,
* 也就是 HeaderToken(不配置表示当前局部过滤器不生效)
* @author xieyucan
* <br>CreateDate 2022/9/6 9:22
*/
@Component
public class HeaderTokenGatewayFilterFactory
extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeaderTokenGatewayFilter();
}
}
全局过滤器(实现GlobalFilter, Ordered接口声明为@Component即可)
/**
* ProjectName xyc-commerce-springcloud
* 全局登录鉴权过滤器
* @author xieyucan
* <br>CreateDate 2022/9/6 10:26
*/
@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;
}
/**
* 登录、注册、鉴权
* 1:如果是登录注册,则去授权中心拿token,返回给客户端
* 2:如果是服务其他服务,则鉴权,没有权限返回401
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 如果是登录
if(request.getURI().getPath().contains(GatewayConstant.LOGIN_URI))
{
//去授权中心获取token
String token = getTokenAuthorityCenter(request,
GatewayConstant.AUTHORITY_CENTER_TOKEN_FORMAT);
//header 中不能设置为null
response.getHeaders().
add(CommonConstant.JWT_USER_INFO_KEY,token==null?"null":token);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
//如果是注册
if(request.getURI().getPath().contains(GatewayConstant.REGISTER_URI))
{
//去授权中心拿token,先创建用户,再返回token
String token = getTokenAuthorityCenter(request,
GatewayConstant.AUTHORITY_CENTER_REGISTER_FORMAT);
response.getHeaders()
.add(CommonConstant.JWT_USER_INFO_KEY,token==null?"null":token);
response.setStatusCode(HttpStatus.OK);
}
//访问其他服务,则鉴权、校验是否能够从token中解析用户信息
String token=request.getHeaders().getFirst(CommonConstant.JWT_USER_INFO_KEY);
LoginUserInfo loginUserInfo=null;
try {
//解析token
loginUserInfo= TokenParseUtil.parseUserInfoFromToken(token);
}catch (Exception ex){
log.error("parse user info from token error:[{}]",ex.getMessage(),ex);
}
//从token获取不到用户登录信息 返回401
if(loginUserInfo==null)
{
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//解析通过则放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE+2;
}
/**
* 从post请求中获取到请求数据
* @param request
* @return
*/
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(buffer);
bodyRef.set(charBuffer.toString());
});
//获取 request body
return bodyRef.get();
}
/**
* 从授权中心获取token
* @param request
* @param uriFormat
* @return
*/
private String getTokenAuthorityCenter(ServerHttpRequest request,String uriFormat)
{
/**
* service id就是服务名称,这里实现了负载均衡
*/
ServiceInstance instance = loadBalancerClient
.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
log.info("nacos client info:[{}],[{}],[{]]",
instance.getServiceId(),
instance.getInstanceId(),
instance.getMetadata());
String requestUrl=String.format(uriFormat,instance.getHost()
,instance.getPort());
UsernameAndPassword usernameAndPassword=JSON.parseObject(
parseBodyFromRequest(request),
UsernameAndPassword.class);
log.info("login request url and body:[{}],[{}]",requestUrl,
JSON.toJSONString(usernameAndPassword));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
JwtToken token = restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), httpHeaders),
JwtToken.class);
if(null!=token)
{
return token.getToken();
}
return null;
}
}