前言
Swagger在API文档生成及测试方面非常方便,但是很多的API调用都需要用到token验证,然后经过Gateway网关,鉴权验证通过之后访问业务系统。为了方便后端开发自测接口,我们可以免去鉴权吗?答案是可以的!
一般鉴权方式
我们先看看如果需要鉴权,应该怎么做呢?
可以在swagger config类里边使用 globalOperationParameters() 方法来配置全局参数token:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(Arrays.asList(
new ParameterBuilder()
.name("token")
.description("授权")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build()
))
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("接口文档")
.description("接口文档")
.version("1.0.0")
.build();
}
}
也可以使用 securitySchemes() 方法来配置安全协议token:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
.paths(PathSelectors.any())
.build()
.securitySchemes(Arrays.asList(
new ApiKey("Bearer", "token", "header")
))
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("接口文档")
.description("接口文档")
.version("1.0.0")
.build();
}
}
请求头有了token,一般项目的话,请求发到服务端做鉴权,微服务的话,访问服务端之前,需要在Gateway鉴权,我们先了解下什么是Gateway,他的作用又是什么呢。
Gateway是什么?
官方解释Gateway微服务中必不可少的一个组件,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,具有参数校验、权限校验、流量监控、日志输出、协议转换、响应内容、响应头修改等功能。
那我们想想,之前没有微服务的时候,这些功能不是在业务系统里面也能实现吗?为什么现在都要放在Gateway中来呢?没错,为了解耦!业务系统专注于系统本身,网关作为接收客户端请求和业务系统的中间层,这些”杂事“交给Gateway统一管理再合适不过了。
Gateway三大核心
1.Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
2.Predicate(断言):参考的是Java8的java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
3.Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前(pre)或者之后(post)对请求进行修改,目前开放出来的过滤参数有兴趣可以了解下,https://blog.csdn.net/m0_67402096/article/details/126496872。
Gateway配置示例
###########应用配置 start###########
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: 39-base
uri: http://10.43.117.39:8001
predicates:
- Path=/39/projectName/**
filters:
- StripPrefix=2
- name: AddUserName
args:
enabled: true
shouldSkipUrls:
- /v2/api-docs/**
- /swagger-ui.html*
- /webjars/**
- /actuator/**
- /swagger-resources/**
- /i18n/**
- /login-free/**
- /cloud-test/findPrintList*
- /persons/resetPassword**
- /cloud-test/**
- /t-role-apply/apsPlanPageInfo
- /IAM-app-user/**
- /flotsam/dataTransferBPMPS
- /qrCode/**
###########应用配置 end###########
解读配置
现在有一个服务39-base部署在本机,地址和端口为10.43.117.39:8081,所以路由配置uri为http://10.43.117.39:8081
使用网关服务路由到此服务,predicates -Path=/39/projectName/**,网关服务的端口为8080,启动网关服务,访问10.43.117.39:8080/39/projectName,路由断言就会将请求路由到39-base。
直接访问39-base的接口10.43.117.39:8081/swagger-ui.html#/api/test,通过网关的访问地址则为10.43.117.39:8080/39/projectName/swagger-ui.html#/api/test,predicates配置将请求断言到此路由,filters-StripPrefix=1代表将地址中/后的第一个截取,所以39/projectName就截取掉了。
那我们知道了,鉴权其实就是一种过滤规则,或者说是校验规则。那怎么做到免鉴权的呢,其实就是在Filter中配置相应的参数,但是目前Gateway没有开放相应的参数,比较好的是,它支持自定义参数,具体是怎么做的呢,我们往下看:
1、配置yml,如上图所示,配置的shouldSkipUrls参数;
2、配置文件:NotAuthUrlPropertie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashSet;
@Data
@Component
@ConfigurationProperties("agilepay.gateway")
public class NotAuthUrlProperties {
private LinkedHashSet<String> shouldSkipUrls;
}
3、重写GlobalFilter.Filter方法。(Gateway提供GlobalFilter(实现过滤器业务)及Ordered(定义过滤器执行顺序)两个接口用来定义过滤器,我们自定义过滤器只需要实现这个两个接口即可。)
@Component
@Slf4j
public class WrapperRequestGlobalFilter implements GlobalFilter, Ordered {//, Ordered
@Autowired
private RsaKeyMapper rsaKeyMapper;
@Resource
private NotAuthUrlProperties notAuthUrlProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String currentUrl = exchange.getRequest().getURI().getPath();
//1:不需要认证的url,不过滤放行的url
if (shouldSkip(currentUrl)) {
log.info("==========已跳过url{}=====", currentUrl);
return chain.filter(exchange);
}
log.info("===========网关请求解密开始==================");
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 0;
}
/**
* 方法实现说明:不需要过滤的路径
* <p>
* // * @param currentUrl 当前请求路径
*/
private boolean shouldSkip(String currentUrl) {
PathMatcher pathMatcher = new AntPathMatcher();
for (String skipPath : notAuthUrlProperties.getShouldSkipUrls()) {
if (pathMatcher.match(skipPath, currentUrl)) {
return true;
}
}
return false;
}
}
使用gateway通过配置文件即可完成路由的配置,非常方便,我们只要充分的了解配置项的含义及规则就可以了;但是这些配置如果要修改则需要重启服务,重启网关服务会导致整个系统不可用,这一点是无法接受的,下面介绍如何通过Nacos实现动态路由。
使用nacos结合gateway-server实现动态路由,我们需要先部署一个nacos服务,可以使用docker部署或下载源码在本地启动,具体操作可以参考官方文档即可。
Nacos_Gateway配置
Nacos实现动态路由的方式核心就是通过Nacos配置监听,配置发生改变后执行网关相关api创建路由
Nacos配置监听
@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(NacosDynamicRouteService.class);
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher applicationEventPublisher;
/** 路由id */
private static List<String> routeIds = Lists.newArrayList();
/**
* 监听nacos路由配置,动态改变路由
* @param configInfo
*/
@NacosConfigListener(dataId = "routes", groupId = "gateway-server")
public void routeConfigListener(String configInfo) {
clearRoute();
try {
List<RouteDefinition> gatewayRouteDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
addRoute(routeDefinition);
}
publish();
LOGGER.info("Dynamic Routing Publish Success");
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
/**
* 清空路由
*/
private void clearRoute() {
for (String id : routeIds) {
routeDefinitionWriter.delete(Mono.just(id)).subscribe();
}
routeIds.clear();
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 添加路由
*
* @param definition
*/
private void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
routeIds.add(definition.getId());
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
/**
* 发布路由、使路由生效
*/
private void publish() {
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
}
}
总结
Swagger免鉴权是重写了Gateway中GlobalFilter接口Filter方法,方法中用当前请求url和配置免鉴权的url做匹配,匹配到则免鉴权;整合Nacos及配置监听就可以实现动态路由了。