介绍
spring cloud gateway是spring cloud的一个api网关组件,替换Zuul开发的网关服务,基于Spring5.0 + SpringBoot2.0 + WebFlux(基于性能的Reactor模式响应式通信框架Netty,异步阻塞模型)等技术开发,性能高于Zuul,旨在为微服务架构提供种简单有效的统的API路由管理式。
调用流程
PS:此处借用一张网上的流程图

可以看到,接收到请求后,会先去遍历配置中的routes,匹配路由信息,判断当前路由是否可用,匹配成功后,会进入过滤器,在过滤器中可以进行各种操作,本文就是在实现WebFilter过滤器,在其中进行接口权限校验等等,通过过滤器后,就开始转发请求到相应的服务当中。
关键实现
具体项目实现请看:项目地址
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gcp</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>gateway</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml(配置)
server:
port: 8700
spring:
application:
name: gateway
profiles:
active: dev
redis:
host: 127.0.0.1
port: 6379
cloud:
nacos:
discovery:
server-addr: 192.168.56.1:8848
group: DEFAULT_GROUP
gateway:
discovery:
locator:
# 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
enabled: true
routes:
# id,自定义,不可重复
- id: security_route
# 转发服务名,nacos中待调用的服务名称
uri: lb://security
# 路由规则
predicates:
- Path=/security/**
filters:
- StripPrefix=1
AuthorizeFilter.class(权限校验过滤器,实现WebFilter)
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.gcp.gateway.config.RedisCache;
import com.gcp.gateway.response.CommonException;
import com.gcp.gateway.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.net.URI;
import java.util.List;
/**
* 权限校验过滤器
* @author gcp
*/
@Order(1)
@Component
@Slf4j
public class AuthorizeFilter implements WebFilter {
@Resource
private RedisCache redisCache;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
URI uri = exchange.getRequest().getURI();
log.info("开始校验路由:{}",uri.getPath());
if(uri.getPath().contains("login")){
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("token");
log.info("接收的token为:{}",token);
if(!StringUtils.hasText(token)){
log.error("token为空");
throw new CommonException("权限不足,请重新登录");
}
String userId;
// 解析token
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
log.error("解析token失败");
throw new CommonException("解析token失败");
}
// 从redis中获取用户信息
String redisKey = "login:" + userId;
redisCache.getCacheObject(redisKey);
JSONObject jsonObject = redisCache.getCacheObject(redisKey);
if (jsonObject.isEmpty()) {
log.error("用户信息获取失败");
throw new CommonException("账户过期,请重新登录");
}
String roleId = exchange.getRequest().getHeaders().getFirst("role_id");
List<String> list = JSONArray.parseArray(jsonObject.get("permissions").toString(),String.class);
// 校验是否有该角色
if (!list.contains(roleId)) {
log.error("角色不匹配");
throw new CommonException("权限不足");
}
String url = uri.getPath();
String realUrl;
realUrl = url.split("\\/")[1];
String role = redisCache.getCacheObject("role:role_" + roleId);
// 校验角色是否存在该路径
if (!role.contains(url.split(realUrl)[1])) {
log.error("该接口路径无权限");
throw new CommonException("权限不足!!");
}
return chain.filter(exchange);
}
}
WebExceptionHandler.class(拦截filter中的自定义异常,并抛出响应状态码)
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gcp.gateway.response.CommonException;
import com.gcp.gateway.response.ResponseCode;
import com.gcp.gateway.response.ResponseModelDto;
import com.gcp.gateway.response.ResponseModels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
/**
* filter异常响应
* @author gcp
*/
@Slf4j
@Order(-1)
@Component
public class WebExceptionHandler implements ErrorWebExceptionHandler {
@Resource
ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
response.setStatusCode(HttpStatus.OK);
ResponseModelDto res;
if (ex instanceof CommonException) {
log.debug("接口调用失败,url={},message={}", exchange.getRequest().getURI().toString(), ex.getMessage());
res = ResponseModels.commonException((CommonException)ex);
} else {
log.error("接口调用失败,url={},message={}", exchange.getRequest().getURI().toString(), ex.getMessage());
res = ResponseModels.commonException().message(ResponseCode.CommonException);
}
HttpHeaders headers = response.getHeaders();
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "*");
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.set(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.set(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
headers.setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
return bufferFactory.wrap(objectMapper.writeValueAsBytes(res));
} catch (JsonProcessingException e) {
log.warn("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}