网关作为整个微服务集群的入口,承担着对外部请求的认证和鉴权职责,同时作为网关,相关配置的频繁变动是必然的,这就要求网关具备高度的灵活性,不能每次配置变动都需要重启网关来实现配置加载。基于以上实际场景,本文引出接下来要介绍的重点——网关节点动态加载网关配置,基本原理是基于nacos
的配置中心和Spring
容器的事件发布监听机制,配置中心的网关配置变动后,触法配置变动事件,gateway
重新加载配置到运行内存中。
如下图所示,不同环境创建不同的命名空间。
这里需要注意的是,这里文件的名字需要和不同环境里配置的dataId
的值保持一致,否则会加载失败。
本文使用以下可能在服务运行期间会发生变动的应用配置:
## 不需要认证的请求
auth.skip.urls=[/login]
## jwt生成key使用到的秘钥
jwt.secret.key=q3t6w9z$C&F)J@NcQfTjWnZr4u7x
## jwt的token过期时间
jwt.refresh.token.expireTime=1000
网关集成动态加载配置需要基于Spring
容器的事件发布监听机制,配置更改事件发布后,触发RouteDefinitionWriter
更新内存中的配置值。其中RouteDefinitionWriter
的默认实现为InMemoryRouteDefinitionRepository
,该类的功能是持有网关运行期间使用到的各种应用配置,其底层数据结构是一个HashMap
,属性名为key
,RouteDefinition
为包装了属性值的Bean
,在下文中将详细介绍如何实现动态配置加载。
首选,需要定义属性配置类及相关配置:
@Data
@ConfigurationProperties(prefix="nacos")
@Configuration
public class NacosGatewayProperties {
private String address;
private String dataId;
private String groupId;
private Long timeout;
}
配置值如下所示,网关服务启动后,根据以下配置去nacos
拉取不同环境的配置,此处需结合spring.profiles.active
使用,本文不再展开,下文中以prod配置文件演示。
nacos.address=127.0.0.1
nacos.dataId=spring-cloud-gateway-prod.properties
nacos.groupId=DEFAULT_GROUP
nacos.timeout=10
在nacos
相关配置中加入命名空间区分不同环境的配置文件,该命名空间同样需要根据不同环境进行相关的配置。
spring.cloud.nacos.config.namespace=c842a7b3-672d-4e51-81f5-2af3b7573bc7
动态路由配置核心类如下:
@Component
@Slf4j
public class DynamicRouteConfig {
@Autowired
private DynamicRoutePublisher dynamicRoutePublisher;
@Autowired
private NacosGatewayProperties nacosGatewayProperties;
@PostConstruct
public void init(){
// 在服务启动时,其拉取相关配置
dynamicRouteByNacosListener();
}
/**
* 监听Nacos Server下发的动态路由配置
*/
public void dynamicRouteByNacosListener (){
try {
ConfigService configService= NacosFactory.createConfigService(nacosGatewayProperties.getAddress());
String content = configService.getConfig(nacosGatewayProperties.getDataId(), nacosGatewayProperties.getGroupId(), nacosGatewayProperties.getTimeout());
log.info("配置参数[{}]",content);
configService.addListener(nacosGatewayProperties.getDataId(), nacosGatewayProperties.getGroupId(), new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> list = JSON.parseArray(configInfo, RouteDefinition.class);
list.forEach(definition->{
dynamicRoutePublisher.update(definition);
});
}
@Override
public Executor getExecutor() {
return null;
}
});
} catch (NacosException e) {
e.printStackTrace();
}
}
}
@Component
public class DynamicRoutePublisher implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
/**
* 增加路由
* @param definition
* @return
*/
public String add(RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 更新路由
* @param definition
* @return
*/
public String update(RouteDefinition definition) {
try {
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
return "update fail,not find route routeId: "+definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}
/**
* 删除路由
* @param id
* @return
*/
public String delete(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id));
return "delete success";
} catch (Exception e) {
e.printStackTrace();
return "delete fail";
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}
以上两个类的核心功能是:当用户在nacos
配置中心进行相关配置更改后,触发配置变更事件,将旧的配置删除,保存新的配置。
因为配置会发生变动,所以配置的使用与传统方式有所区别,本文以登录接口解释:
@Service
@Slf4j
public class LoginServiceImpl implements LoginService {
private final String LOGIN_USER_KEY = "login:user:%s";
private final long refreshTokenExpireTime=1000;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private EchoService echoService;
@Autowired
private ConfigurableApplicationContext applicationContext;
@Override
public LoginUser login(String userName, String password) {
//生成JWT
log.info("秘钥[{}]",applicationContext.getEnvironment().getProperty("jwt.secret.key"));
String token = JwtTokenUtil.createToken(userName,applicationContext.getEnvironment().getProperty("jwt.secret.key"));
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
String refreshTokenKey = String.format(LOGIN_USER_KEY, refreshToken);
redisTemplate.opsForHash().put(refreshTokenKey,
"token", token);
redisTemplate.opsForHash().put(refreshTokenKey,
"userName", userName);
//refreshToken设置过期时间
String refreshTokenExpireTime=applicationContext.getEnvironment().getProperty("jwt.refresh.token.expireTime");
if(StringUtils.isBlank(refreshTokenExpireTime)){
return null;
}
redisTemplate.expire(refreshTokenKey,
Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
return LoginUser.builder()
.userName(userName)
.userToken(token)
.loginTime(new Date())
.build();
}
}
通过装载入ConfigurableApplicationContext
,通过配置上下文拿到相关的配置key
,进行获取配置值。
如下图所示,在更改前jwt.refresh.token.expireTime
的值为1000,更改后为10000。
查看网关日志,如图中红箭头所示,
使用Postman调用登录接口通过断点查看配置值是否真的发生变动。
可以很清晰地看到,配置值在不重新启动的条件下,已经发生变化,在有多个服务实例的情况下,所以节点同步更新。
本文通过简单的演示,展现了通过动态加载配置这一功能,在统一的配置中心进行配置管理,与传统的配置文件相比,不仅极大地提高了服务变更的灵活性,而且通过Nacos提供的可视化界面,所以环境的配置管理变得更加简单,降低了运维同学的管理难度。