阅读提醒:
- 本文面向的是有一定springboot基础者
- 本次教程使用的Spring Cloud Hoxton RELEASE版本
- 由于knife4j比swagger更加友好,所以本文集成knife4j
- 本文依赖上一篇的工程,请查看上一篇文章以做到无缝衔接,或者直接下载源码:https://github.com/WinterChenS/spring-cloud-hoxton-study
前情概要
- SpringCloud系列教程(一)开篇
- SpringCloud系列教程(二)之Nacos | 8月更文挑战
- # SpringCloud系列教程(三)之Open Feign | 8月更文挑战
- SpringCloud系列教程(四)之SpringCloud Gateway | 8月更文挑战
本文概览
- 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;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//获取所有路由的ID
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
gatewayProperties.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;
@Autowired
public 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 {
@Autowired
UserRedisCollection 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;
}
@Override
public 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获取token
String 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;
}
@Override
public 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)
@Autowired
private 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
参考文献
0 Comments
Leave a comment