之所以想写这一系列,是因为之前工作过程中有几次项目是从零开始搭建的,而且项目涉及的内容还不少。在这过程中,遇到了很多棘手的非业务问题,在不断实践过程中慢慢积累出一些基本的实践经验,认为这些与业务无关的基本的实践经验其实可以复刻到其它项目上,在行业内可能称为脚手架,因此决定将此java基础脚手架的搭建总结下来,分享给大家使用。
注意:由于框架不同版本改造会有些使用的不同,因此本次系列中主要使用基本框架是 spring-boo-2.3.12.RELEASE和spring-cloud.-Hoxton.SR12,所有代码都在commonFramework项目上:https://github.com/forever1986/commonFramework/tree/master
目录
1 gateway
之所以将gateway放在这里讲,是因为承接上一篇的OAuth2,其中gateway有一个重要的功能就是鉴权,可以结合OAuth2进行鉴权。
在我们使用微服务架构的时候,免不了需要一个网关,而对于java项目来说,gateway是一个很好的选择。一般实践中,gateway有以下功能:
- 路由转发:根据请求的特定条件(如 URL路径、请求参数、请求头等)来将请求转发到后端的多个服务,并支持动态路由配置
- 过滤功能:提供了一套过滤器机制,允许开发人员对请求进行修改和验证,以及应用各种策略,如认证、安全、监控/指标、限流、日志、请求转发/重试等
- 容错处理:集成断路器(Hystrix),为微服务网关提供容错处理的功能
- 服务发现:与服务注册中心集成,动态从服务注册中心获取服务信息并进行路由
- 请求转发:进行请求的协议转换,比如将 HTTP 请求转换成 WebSocket 请求
- 请求限流:支持通过配置限流规则,对请求进行限流,防止恶意请求或异常情况下的流量冲击
下面以动态读取路由配置+过滤功能,实现gateway的路由转发可以动态加载,以及实现鉴权功能
2 动态加载路由表
参考gateway子模块
在实际应用中,我们不可能将路由配置放入项目的yaml配置文件中,这样修改路由时,都需要重新启动或者发布。这时候,配置中心就起到动态配置路由转发的存储功能,同时gateway也提供RouteDefinitionRepository可进行路由表动态加载。
原理:将路由配置文件放入nacos中,gateway子模块通过实现RouteDefinitionRepository,监听nacos的路由配置文件,实现动态更新路由配置
1)创建gateway子模块,引入以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--监听配置文件的更新 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--获取配置文件 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2)配置bootstrap.yml,要配置服务发现和配置中心的信息
server:
port: 9991
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
config:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
file-extension: yaml
group: DEFAULT_GROUP
prefix: ${spring.application.name}
3)实现NacosRouteDefinitionRepository(实现RouteDefinitionRepository),用于自动监听配置文件并刷新路由表
package com.demo.route.config;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* nacos路由数据源
*/
@Component
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {
private static final Logger logger = LoggerFactory.getLogger(NacosRouteDefinitionRepository.class);
private static final String SCG_DATA_ID = "cloud-gateway-service";
private static final String SCG_GROUP_ID = "DEFAULT_GROUP";
private final ApplicationEventPublisher publisher;
private final NacosConfigManager nacosConfigManager;
public NacosRouteDefinitionRepository(ApplicationEventPublisher publisher, NacosConfigManager nacosConfigManager) {
this.publisher = publisher;
this.nacosConfigManager = nacosConfigManager;
addListener();
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitionList = new ArrayList<>(0);
try {
String configContent = nacosConfigManager.getConfigService().getConfig(SCG_DATA_ID, SCG_GROUP_ID, 5000);
if (StringUtils.isNotEmpty(configContent)) {
routeDefinitionList = JSON.parseArray(configContent, RouteDefinition.class);
}
} catch (NacosException e) {
logger.error("从Nacos加载配置的动态路由信息异常", e);
}
return Flux.fromIterable(routeDefinitionList);
}
/**
* 添加Nacos监听
*/
private void addListener() {
try {
nacosConfigManager.getConfigService()
.addListener(SCG_DATA_ID, SCG_GROUP_ID, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
});
} catch (NacosException e) {
logger.error("添加Nacos监听异常", e);
}
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return null;
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return null;
}
}
4)设置GatewayApplication设置@EnableDiscoveryClient注册到nacos
5)在nacos的public命名空间和DEFAULT_GROUP分组中,配置nacos的路由配置名为:cloud-gateway-service的json文件
[
{
"id": "baidu",
"order": 0,
"predicates": [{
"args": {
"pattern": "/baidu/**"
},
"name": "Path"
}],
"uri": "http://www.baidu.com",
"filters": [{
"args": {
"_genkey_0": "2"
},
"name": "StripPrefix"
}]
},
{
"id": "manager",
"predicates": [{
"args": {
"pattern": "/manager/**"
},
"name": "Path"
}],
"uri": "http://localhost:9981",
"filters": [{
"args": {
"_genkey_0": "1"
},
"name": "StripPrefix"
}]
}
]
6)通过请求以下访问,可以得到转发;同时你可以在nacos上面修改cloud-gateway-service的json文件路由,可以测试动态刷新
http://localhost:8891/baidu/aa
http://localhost:8891/business/business
3 鉴权功能
参考gateway子模块
网关的过滤功能:提供了一套过滤器机制,允许开发人员对请求进行修改和验证,以及应用各种策略,如认证、安全、监控/指标、限流、日志、请求转发/重试等。我们这里的鉴权就是使用过滤功能。在配置鉴权功能之前,我们有必要了解一下内容:
- 鉴权原理:在系列8中我们讲过security、JWT、oauth的内容,这里利用auth-authentication子模块生成token,我们知道验证token的有效性2种方法,一种是使用访问auth-authentication子模块的access_token接口验证,一种是通过RSA非对称加密验证。这里的token采用RSA非对称加密验证(这是因为本身路由访问非常频繁的服务,不适合频繁访问access_token接口)。
- gateway过滤:在gateway中有3个比较重要的过滤:ReactiveAuthenticationManager->ReactiveAuthorizationManager->Gateway Filters。
1)其中ReactiveAuthenticationManager用于封装JWT为OAuth2Authentication并判断Token的有效性;
2)ReactiveAuthorizationManager用于基于URL的鉴权;
3)Gateway Filters是网关的过滤流程。
下面我们通过配置JWT+RSA的解密对token进行认证,另外通过实现ReactiveAuthorizationManager对其进行鉴权。
1)引入以下依赖:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
3)编写ResourceServerManager(实现ReactiveAuthorizationManager),实现其check接口,获得用户信息,对权限进行判断(这里只是演示一下,没有实际权限判断代码,实际业务中可以加入一些权限判断,比如路由权限、RABC等业务判断功能)
4)编写ResourceServerConfig,通过注入securityWebFilterChain类,将JWT认证和ResourceServerManager都设置到过滤链中
5)在ResourceServerConfig中,通过注入jwtAuthenticationConverter,实现JWT+RSA的公钥,对token进行认证
6)编写SecurityGlobalFilter(实现GlobalFilter,和Ordered),做最后处理(当然如果没有此需求,也可以不做处理)
7)通过请求以下访问,可以得到转发(前提是你需要在header中增加token)
http://localhost:8891/baidu/aa
http://localhost:8891/business/business