使用Spring Gateway完成网关的登录、注册和鉴权
三大核心概念
Route、Predicate、Filter
-
Route(路由):是构建网关的基本模块,由ID、URI、一系列的断言和过滤器组成
-
Predicate(断言):可以匹配Http请求中所有的内容(请求头、参数等等),请求和断言相匹配则通过当前断言
-
Filter(过滤器):包括全局过滤器和局部过滤器,可以在请求被路由的前后对请求进行修改
Spring Cloud Gateway过滤器
全局过滤器和局部过滤器
-
全局过滤器作用于所有的路由,不需要单独配置,通常用来实现统一化才处理的业务需求
-
局部过滤器实现并生效的三步骤
-
需要实现GatewayFliter,Ordered,实现相关的方法
-
加入过滤器工厂,并且将工厂注册到Spring容器中
-
在配置文件中进行配置,如果不配置则不启用此过滤器规则(路由规则)
-
Spring Cloud Gateway路由的配置
常见的三种配置方式
-
在代码中注入
RouteLocator Bean
,并手工编写配置路由定义 -
在
application.yml
、bootstrap.yml
等配置文件中配置spring.cloud.gateway -
通过配置中心(Nacos)实现动态的路由配置
创建局部过滤器
/**
* @description: 请求头部携带token的验证过滤器,局部过滤器
* @author qingtian
* @date 2021/12/13 18:04
* @version 1.0
*/
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//从http header中寻找key为token value为imooc的键值对
String name = exchange.getRequest().getHeaders().getFirst("token");
if("imooc".equals(name)) {
return chain.filter(exchange);
}
//标记此次请求无权限
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
}
使用局部过滤器时我们必须要加入过滤器工厂,并且将它放入Spring容器中
/**
* @author qingtian
* @version 1.0
* @description: 使用局部过滤器必须要配置,否则不生效
* @date 2021/12/13 18:21
*/
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeaderTokenGatewayFilter();
}
}
创建全局过滤器
/**
* @author qingtian
* @version 1.0
* @description: 缓存RequestBody的全局过滤器
* @date 2021/12/13 18:30
*/
@Slf4j
@Component
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//判断请求的路径中是否含有login或者是register
boolean isLoginOrRegister = exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
|| exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);
if (null == exchange.getRequest().getHeaders().getContentType()
|| !isLoginOrRegister) {
return chain.filter(exchange);
}
//DataBufferUtils.join()拿到请求中的数据
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
//确保数据缓冲区的数据不被释放DataBufferUtils.retain
DataBufferUtils.retain(dataBuffer);
//defer just创建数据源,得到当前数据的副本
Flux<DataBuffer> cachedFlux = Flux.defer(() ->
Flux.just(dataBuffer.slice(0,dataBuffer.readableByteCount())));
//重新包装ServerHttp请求,重写getBody方法,能狗返回请求数据
ServerHttpRequest mutateRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
//将包装后的serverhttpRequest向下传递
return chain.filter(exchange.mutate().request(mutateRequest).build());
});
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 1;
}
}
/**
* @author qingtian
* @version 1.0
* @description: 全局鉴权过滤器
* @date 2021/12/21 23:22
*/
@Component
@Slf4j
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
/**
* @description: 从注册中心中获取对应服务的实例信息
* @date 2021/12/21 23:42
*/
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
/**
* @description: 登录,注册,鉴权
* 1. 如果是登录或注册,则去注册中心拿到token并返回给客户端
* 2. 如果是访问其他的服务,则鉴权,没有权限返回401
* @param: exchange
* @param: chain
* @return: reactor.core.publisher.Mono<java.lang.Void>
* @date: 2021/12/22 0:17
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//如果是登录
if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
//去授权中心拿token
String token = getTokenFromAuthorityCenter(
request,GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
);
//header 中不能设置null
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
//如果是注册
if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
//去注册中心拿token: 会先创建用户,在返回token
String token = getTokenFromAuthorityCenter(
request,GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
);
//header中不允许为null对象
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
response.setComplete();
}
//带着token去访问其他的服务,则鉴权
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
LoginUserInfo loginUserInfo = null;
try {
loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
} catch (Exception ex) {
log.error("parse user info from token has error : [{}]",ex.getMessage(),ex);
}
//获取不到用户信息
if (null == loginUserInfo) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//解析通过放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
//因为在之前缓存body数据的过滤器中是 + 1 ,这个过滤器必须在它之后执行,所以这里是 + 2.
return HIGHEST_PRECEDENCE + 2;
}
/**
* @description: 从post请求中获取到数据
* @param: request
* @return: java.lang.String
* @date: 2021/12/21 23:35
*/
private String parseBodyFromRequest(ServerHttpRequest request) {
//获取请求体
Flux<DataBuffer> body = request.getBody();
//获取原子引用
AtomicReference<String> atomRef = new AtomicReference<>();
//订阅缓冲去去消费请求体中的数据
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
//由于在之前的GlobalCacheRequestBodyFilter中将Body进行了retain所以在这里必须要释放掉
//否则会造成内存泄露
DataBufferUtils.release(buffer);
atomRef.set(charBuffer.toString());
});
//获取到request body
return atomRef.get();
}
/**
* @description: 从授权中心获取 token
* @param: request
* @param: urlFormat
* @return: java.lang.String
* @date: 2021/12/21 23:38
*/
private String getTokenFromAuthorityCenter(ServerHttpRequest request,String urlFormat) {
//负载均衡
ServiceInstance serviceInstance = loadBalancerClient.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
log.info("Nacos Client Info : [{}],[{}],[{}]",
serviceInstance.getServiceId(),serviceInstance.getInstanceId(),
JSON.toJSONString(serviceInstance.getMetadata()));
//拼接请求的url
String requestUrl = String.format(
urlFormat,serviceInstance.getHost(),serviceInstance.getPort()
);
UsernameAndPassword requestBody = JSON.parseObject(
parseBodyFromRequest(request),UsernameAndPassword.class
);
log.info("login request url and body : [{}],[{}]",requestUrl,JSON.toJSONString(requestBody));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JwtToken token = restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(requestBody),headers),
JwtToken.class
);
if (null != token) {
return token.getToken();
}
return null;
}
}
这里两个全局过滤器是协同的,我们首先在
request
的请求中缓存了requestBody
,然后在之后的过滤器中判断请求是登录,注册还是鉴权,去授权中心拿到token
并且返回。
一些常量
/**
* @author qingtian
* @version 1.0
* @description: 定义网关常量
* @date 2021/12/14 15:15
*/
public class GatewayConstant {
/**
* @description: 登录uri
* @date 2021/12/14 15:20
*/
public static final String LOGIN_URI = "/e-commerce/login";
/**
* @description: 注册uri
* @date 2021/12/14 18:52
*/
public static final String REGISTER_URI = "/e-commerce/register";
/**
* @description: 去授权中心拿到token的格式化接口
* @date 2021/12/14 18:58
*/
public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
"http://%s:%s/ecommerce-authority-center/authority/token";
/**
* @description: 去授权中心注册并拿到token的rurl格式化接口
* @date 2021/12/14 21:57
*/
public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
"http://%s:%s/ecommerce-authority-center/authority/register";
}
/**
* @author qingtian
* @version 1.0
* @description: Gateway注册到容器中的bean
* @date 2021/12/21 23:39
*/
@Configuration
public class GatewayBeanConf {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
我们发现在
GatewayConstant
中的LOGIN_URI
和REGISTER_URI
我们并没有配置相应的配置让它们起作用,我们选择使用
在代码中注入RouteLocator Bean
,并手工编写配置路由定义
/**
* @author qingtian
* @version 1.0
* @description: 配置登录请求转发规则
* @date 2021/12/25 23:58
*/
@Configuration
public class RouteLocatorConfig {
/**
* @description: 使用代码定义路由规则,在网关层面上拦截登录和注册接口
* @param: builder
* @return: org.springframework.cloud.gateway.route.RouteLocator
* @date: 2021/12/26 0:00
*/
@Bean
public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
//手动定义Gateway路由规则需要指定id,path 和url
return builder.routes()
.route(
"e_commerce_authority",
r -> r.path(
"/imooc/e-commerce/login",
"/imooc/e-commerce/register"
).uri("http://localhost:9001/")
).build();
}
}
至此
至此我们已经完成了使用
Spring Gateway
对请求的登录,注册和鉴权功能的编写。
评论区