阅读提醒:

  1. 本文面向的是有一定springboot基础者
  2. 本次教程使用的Spring Cloud Hoxton RELEASE版本
  3. 由于knife4j比swagger更加友好,所以本文集成knife4j
  4. 本文依赖上一篇的工程,请查看上一篇文章以做到无缝衔接,或者直接下载源码:https://github.com/WinterChenS/spring-cloud-hoxton-study

前情概要

  • SpringCloud系列教程(一)开篇
  • SpringCloud系列教程(二)之Nacos | 8月更文挑战
  • SpringCloud系列教程(三)之Open Feign | 8月更文挑战
  • SpringCloud系列教程(四)之SpringCloud Gateway | 8月更文挑战
  • SpringCloud系列教程(五)之SpringCloud Gateway 网关聚合开发文档 swagger knife4j 和登录权限统一验证
  • SpringCloud系列教程(六)之SpringCloud 使用sentinel作为熔断器
  • SpringCloud系列教程(七)之使用Spring Cloud Sleuth+Zipkin实现链路追踪
  • SpringCloud系列教程(八)之整合seata分布式事务

本文概览

  • Spring Cloud Gateway集成Knife4j
  • Spring Cloud Gateway集成登录权限统一校验

开始

上篇文章介绍了Spring Cloud Gateway的使用,本文将介绍如何在网关层聚合swagger文档,聚合之后可以非常方便的对开发文档进行管理,也是业界比较常用的方式。

首先在父pom文件dependencyManagement 节点中增加knife4j的依赖:

<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version>
</dependency>

配置 spring-cloud-nacos-provider和spring-cloud-nacos-consumer

注意,这里标题中两个工程为前几篇文章中构建的,如果不想看前几篇文章,就创建两个模块然后分别集成nacos,knife4j即可。

在两个模块中分别增加maven依赖:

<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId></dependency>

在两个模块中分别新增配置类:Knife4jConfiguration

@Configuration
public class Knife4jConfiguration {@Value("${swagger.enable:true}")private boolean enableSwagger;@Bean(value = "defaultApi2")public Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(new ApiInfoBuilder().title("provider服务").version("1.0").build()).enable(enableSwagger).select().apis(RequestHandlerSelectors.basePackage("com.winterchen.nacos.rest")).paths(PathSelectors.any()).build();}}

注意有些相关的信息需要修改。

新增配置:

swagger:enable: true

配置 spring-cloud-gateway

接下来是重头戏,如何在Spring Cloud Gateway中聚合swagger文档。

首先在pom中增加maven依赖:

<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

新增Swagger的配置类:SwaggerResourceConfig

@Component
@Primary
public class SwaggerResourceConfig implements SwaggerResourcesProvider {private final RouteLocator routeLocator;private final GatewayProperties gatewayProperties;public SwaggerResourceConfig(RouteLocator routeLocator, GatewayProperties gatewayProperties) {this.routeLocator = routeLocator;this.gatewayProperties = gatewayProperties;}@Overridepublic List<SwaggerResource> get() {List<SwaggerResource> resources = new ArrayList<>();List<String> routes = new ArrayList<>();//获取所有路由的IDrouteLocator.getRoutes().subscribe(route -> routes.add(route.getId()));//过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResourcegatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {route.getPredicates().stream().filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())).forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("**", "v2/api-docs"))));});return resources;}private SwaggerResource swaggerResource(String name, String location) {SwaggerResource swaggerResource = new SwaggerResource();swaggerResource.setName(name);swaggerResource.setLocation(location);swaggerResource.setSwaggerVersion("2.0");return swaggerResource;}
}

主要的配置的作用已经在代码中进行注释。

新增一个控制器:SwaggerHandler

@RestController
public class SwaggerHandler {@Autowired(required = false)private SecurityConfiguration securityConfiguration;@Autowired(required = false)private UiConfiguration uiConfiguration;private final SwaggerResourcesProvider swaggerResources;@Autowiredpublic SwaggerHandler(SwaggerResourcesProvider swaggerResources) {this.swaggerResources = swaggerResources;}/*** Swagger安全配置,支持oauth和apiKey设置*/@GetMapping("/swagger-resources/configuration/security")public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {return Mono.just(new ResponseEntity<>(Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));}/*** Swagger UI配置*/@GetMapping("/swagger-resources/configuration/ui")public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {return Mono.just(new ResponseEntity<>(Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));}/*** Swagger资源配置,微服务中这各个服务的api-docs信息*/@GetMapping("/swagger-resources")public Mono<ResponseEntity> swaggerResources() {return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));}
}-----------------

测试

分别运行:spring-cloud-nacos-provider,spring-cloud-nacos-consumer,spring-cloud-gateway 三个服务。

打开swagger的地址: http://127.0.0.1:15010/doc.html

验证结果:

看到上图中的结果说明聚合成功。

集成登录权限的统一校验

为了直达主题,本次集成登录权限会尽量的简单,其中忽略一些细节,比如登录服务模块,可以查看demo的源码:https://github.com/WinterChenS/spring-cloud-hoxton-study/tree/main/spring-cloud-auth
并且我会在关键的点上明确讲解。

在开始之前需要明白一个原理,如果要实现统一鉴权,那么需要对所有的请求进行统一的拦截,要实现统一的拦截,在Spring Cloud Gateway中有一个filter接口为:GlobalFilter

所以我们可以通过实现GlobalFilter 接口来实现请求的拦截。

实现

新建一个类实现GlobalFilter 接口,并且实现filter 方法,在此基础上我们还需要实现Ordered 接口,控制拦截的优先级,鉴权拦截优先级是最高的,

@Slf4j
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {@AutowiredUserRedisCollection userRedisCollection;// (1)private boolean checkWhiteList(String uri) {boolean access = false;if (uri.contains("/login") || uri.contains("/v2/api-docs")) {access = true;if (uri.contains("logout")) {access = false;}}if (uri.contains("/open-api")) {access = true;}return access;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// (2)ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();String uri = request.getURI().getPath();// (3)//前端访问不到header问题response.getHeaders().add("Access-Control-Allow-Headers","X-PINGOTHER, Origin, X-Requested-With, Content-Type, Accept, token");response.getHeaders().add("Access-Control-Expose-Headers", "token");ServerHttpRequest mutableReq = request.mutate().header(DefaultConstants.IP_ADDRESS, getIpAddress(request)).build();ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();// (4)//检查白名单if (checkWhiteList(uri)) {return chain.filter(mutableExchange);}// (5)//从request获取tokenString accessToken = request.getHeaders().getFirst(DefaultConstants.TOKEN);log.info("AccessToken: [{}]", accessToken);// (6)if (StringUtils.isBlank(accessToken)) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "未登录");}Claims claims = null;try {// (7)claims = JwtUtil.parseJWT(accessToken, DefaultConstants.SECRET_KEY);if (claims == null) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "未登录");}log.info("claims is:{}", claims);if (claims.getSubject().equals(DefaultConstants.USER)){if(claims.get(DefaultConstants.USERID)!=null) {Long userId = Long.parseLong(claims.get(DefaultConstants.USERID).toString());log.info("userId:{}", userId);Map<String, Object> map = Maps.newHashMapWithExpectedSize(1);map.put(DefaultConstants.USERID,userId.toString());// (8)UserInfoEntity userInfo = userRedisCollection.getAuthUserInfoAndCache(userId);//判断是否if (userInfo == null) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "未登录");}// (9)String token = JwtUtil.createToken(DefaultConstants.USER, map, DefaultConstants.SECRET_KEY);response.getHeaders().add(DefaultConstants.TOKEN, token);mutableReq = request.mutate().header(DefaultConstants.USER_ID, String.valueOf(userId)).header(DefaultConstants.IP_ADDRESS, getIpAddress(request)).build();mutableExchange = exchange.mutate().request(mutableReq).build();return chain.filter(mutableExchange);}}} catch (Exception e) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "未登录");}return getVoidMono(response, ResultCodeEnum.UNAUTHORIZED, "未登录");}private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse, ResultCodeEnum resultCode, String responseText) {serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");CommonResult<?> result = CommonResult.failed(resultCode.getCode(), responseText);DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(result).getBytes());return serverHttpResponse.writeWith(Flux.just(dataBuffer));}public String getIpAddress(ServerHttpRequest request) {String ip = request.getHeaders().getFirst("x-forwarded-for");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddress().getHostString();}return ip;}@Overridepublic int getOrder() {return -100;}
}
  • (1) 这个方法可以将白名单加入,当请求到这些url的时候不进行权限的校验
  • (2) 我们在使用Spring Cloud Gateway的时候,注意到过滤器(包括GatewayFilter、GlobalFilter和过滤器链GatewayFilterChain),都依赖到ServerWebExchange,这里filter的设计跟Servlet中的的filter相似,也就是当前过滤器决定是否执行下一个过滤器的逻辑,而ServerWebExchange就是当前请求和响应的上下文,不仅包含了request和response,还包含了一些扩展方法,如代码中可以获取到request和response。
  • (3) 这一步,主要是解决前端无法中header中获取到需要获取的参数。比如token
  • (4) 这里对应了第一步白名单的判断,如果当前请求在白名单就跳过后面的一些权限的判断,直接执行下一个过滤器。
  • (5) 这里很简单,就是从request中获取到token,方便jwt转换成对应的用户信息。
  • (6) 如果请求没有携带token就不予通过。
  • (7) jwt将token转换成用户信息,用于后面的判断以及用户的基本信息。
  • (8) 上面获取到用户的信息之后,根据用户的userId查询redis中是否存在该用户数据,如果不存在表示该用户的用户信息已经过期了,而且从redis查询的时候会重置超时时间(也就保证了只要经常的在线就不需要重新登录,超过设定的时间没有在线,那么就需要重新登录)。注意:这里是需要跟登录服务进行联动,也就是登录成功之后将用户的信息存入redis,然后gateway这边能取到该用户信息。所以需要保证这一点。
  • (9) 这里会重新颁发token,主要是防止token会过期,token的过期时间在jwtUtils中可以设置,所以,前端需要每次请求之后都将新的token作为下一次请求的token。
  • 注意:本文的token是放在header中,前端小伙伴需要从header中取token。

代码中的方法:UserRedisCollection.getAuthUserInfoAndCache(Long userId)

@Autowiredprivate RedisTemplate redisTemplate;public UserInfoEntity getAuthUserInfoAndCache(Long userId) {CommonAssert.meetCondition(userId == null, "未获取到userId");String key = DefaultConstants.USER_INFO_REDIS + userId;UserInfoEntity entity = (UserInfoEntity) redisTemplate.opsForValue().get(key);if (null != entity) {redisTemplate.opsForValue().set(key, entity, 60 * 24 * 60 * 60 * 1000, TimeUnit.MILLISECONDS);return entity;}CommonAssert.meetCondition(true, "当前用户未登陆,未获取到登陆信息");return null;}

该方法简单,就是根据userId获取到用户信息的,成功获取之后会重置超时时间,时长可以根据需要进行修改。

关于本文的登录服务,可以从demo源码中获取:

spring-cloud-hoxton-study/spring-cloud-auth at main · WinterChenS/spring-cloud-hoxton-study

测试

分别启动gateway,provider,auth服务。

首先,测试未登录的情况:

然后进入auth服务,打开登录接口:

打开Headers,复制token

再次切换到provider服务,设置文档的全局参数:

然后刷新一下页面再请求就可以发现,请求成功:

到这里就成功集成了gateway鉴权了。

拓展

  • 至于登出的逻辑,思路是这样的,登出只需要在auth服务中将用户信息从redis清除即可,这样在gateway中查询redis就可以查到用户登录信息已经失效了。
  • 如何在服务中获取当前登录用户信息呢?这个其实挺简单的,一般的做法就是写一个工具类,该工具类从请求的header中获取token,或者在请求阶段就讲userId设置到token中,如果只有token就讲token转换成用户信息,然后根据id从redis中获取用户详细信息,不过一般只需要userId就可以了,这边给个示例:
public static String getUserIdByCurrent() {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();return request.getHeader(ConstantsUtil.USER_ID);}else{return "";}}

总结

本章介绍了如何在gateway中聚合swagger文档以及统一鉴权的集成,内容其实并不多,原本打算分开两篇进行介绍的,swagger部分没有什么需要注意的原理,索性就放在一起,真正重要的点就是Spring Cloud Gateway中的filter的应用,这部分可以通过查找资料详细的了解一下,下一篇将介绍如何使用限流组件Sentinel,这个组件由alibaba提供。

源码地址

https://github.com/WinterChenS/spring-cloud-hoxton-study

参考文献

Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改

SpringCloud系列教程(五)之SpringCloud Gateway 网关聚合开发文档 swagger knife4j 和登录权限统一验证【Hoxton版】相关推荐

  1. C#微信公众号开发系列教程五(接收事件推送与消息排重)

    C#微信公众号开发系列教程五(接收事件推送与消息排重) 原文:C#微信公众号开发系列教程五(接收事件推送与消息排重) 微信公众号开发系列教程一(调试环境部署) 微信公众号开发系列教程一(调试环境部署续 ...

  2. Linux求平方脚本,Linux Shell脚本系列教程(五):数学运算

    这篇文章主要介绍了Linux Shell脚本系列教程(五):数学运算,本文讲解了使用let.(())和[]进行算术运算.使用expr进行算术运算.使用bc进行算术运算三种方法,需要的朋友可以参考下 在 ...

  3. 米思齐(Mixly)图形化系列教程(五)-if……else……与逻辑运算

    目录 比较运算 逻辑运算符 if esle 说明 例子 if程序的嵌套 例子 教程导航 联系我们 比较运算和逻辑运算返回两种结果,条件成立(真true)与不成立(假false) 比较运算 下表显示了支 ...

  4. Android官方开发文档Training系列课程中文版:目录

    原文地址 : http://android.xsoftlab.net/training/index.html 引言 在翻译了一篇安卓的官方文档之后,我觉得应该做一件事情,就是把安卓的整篇训练课程全部翻 ...

  5. Android官方开发文档Training系列课程中文版:创建自定义View之View的创建

    原文地址:http://android.xsoftlab.net/training/custom-views/index.html 引言 Android框架含有大量的View类,这些类用来显示各式各样 ...

  6. Android官方开发文档Training系列课程中文版:OpenGL绘图之图形绘制

    原文地址:http://android.xsoftlab.net/training/graphics/opengl/draw.html 如果你还不清楚如何定义图形及坐标系统,请移步:Android官方 ...

  7. Android官方开发文档Training系列课程中文版:使用Fragment构建动态UI之Fragment创建

    原文地址:http://android.xsoftlab.net/training/basics/fragments/index.html 导言 为了在Android中创建动态的多面板用户界面,你需要 ...

  8. 最新云豹二开直/播短视频完整系统源码+带开发文档/教程

    正文: 最新云豹二开直/播短视频完整系统源码+带开发文档/教程,好友给我分享的,属于云豹二开,功能非常的强大,且有非常完整的开发文档和教程. 但是说实话这类程序不属于好部署的那一种,比较吃一定的技术, ...

  9. 苹果CMS完全开发文档 - 苹果CMS手册 - 苹果CMS教程 - 苹果CMS帮助 - 苹果cmsV10

    苹果CMS完全开发文档 - 苹果CMS手册 - 苹果CMS教程 - 苹果CMS帮助 - 苹果cmsV10 MacCms V10.x 程序介绍 苹果CMS能做什么? 传送门 MacCms V10.x 下 ...

最新文章

  1. 关于Java 获取时间戳的方法,我和同事争论了半天
  2. 织梦怎么改网站主页php,无忧主机教你修改织梦DedeCms网站首页为动态显示的方法...
  3. VIM入门必读(转)
  4. wxWidgets:wxChildFocusEvent类用法
  5. g++默认字符集utf-8_Java可能使用UTF-8作为其默认字符集
  6. tomcat jdbc SlowQueryReport的实现解读
  7. 看门狗计算机丢失xinput13.dll,windows10系统打开程序提示丢失xinput13dll怎么办
  8. matlab论坛真不活跃,MATLAB中文论坛常见问题归纳
  9. 区块链开发公司解析区块链在银行应用的优势
  10. robocopy 备份_windows下使用RoboCopy命令进行文件夹增量备份
  11. zigbee学习之JN5169系统控制器
  12. Pigeon服务的注册与发现
  13. 小程序 加快安卓手机向蓝牙设备发送大数据
  14. 人工智能传奇——关于AI起源与发展的故事
  15. 2022卡塔尔世界杯来了,谁是你心中的夺冠热门球队?
  16. Java虚拟机(三)--------GC算法和收集器
  17. 学习笔记:直面配分函数(待完善)
  18. 数据中心机柜散热解决方案,知道这两点就够了!
  19. 环境污染、空气质量数据集:省/市/县PM2.5浓度、空气流通系数、逆温数据
  20. 雅思口语想考7分,到底该说英音还是美音

热门文章

  1. 首师大计算机科学与技术专业如何,首都师范大学计算机科学与技术
  2. 雅可比(Jacobi)方法
  3. raft partd 日志压缩 部署指导
  4. 【电商】通过商品流转了解系统模块组成
  5. 物联网开发笔记(80)- 使用Micropython开发ESP32开发板之通过IIC接口控制TM1650四位共阴数码管模块
  6. 元宇宙创造大势所趋,一切皆有可能
  7. (附源码)计算机毕业设计ssm毕业生就业管理系统
  8. javascript 实现购物车页面
  9. 汇川PLC功能块 AUTOSHOP 5U系列气缸/真空功能块
  10. java 天干地支_Java-获取年月日对应的天干地支