1.概述
上文我们讲述了spring cloud gateway提供了很多内置的过滤器,但有些时候为了满足个性需求场景,我们需要自定义自己的过滤器,这时我们可以通过自定义GatewayFilterFactory来实现。
对于自定义的factory,我们可以选择去实现接口或继承已有的抽象类,相关的接口是GatewayFilterFactory,而springboot默认帮我们实现的抽象类是AbstractGatewayFilterFactory这个类。
2.自定义GatewayFilterFactory
代码示例如下:
package com.faw.btims.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.faw.btims.gateway.utils.RedisUtil;
import com.faw.btims.gateway.utils.ServletHttpHelper;
import com.faw.comon.constant.CommonConstants;
import com.faw.comon.core.domain.AjaxResult;
import com.faw.comon.exception.ValidateCodeException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.security.auth.login.LoginException;
import java.net.URI;
/**
* FileName: LoginFilter
* Author: TP
* Description:登陆前置校验过滤器
*/
@Component
public class LoginCaptchaFilterGatewayFilterFactory extends AbstractGatewayFilterFactory {
private final static String AUTH_URL = "/auth/login";
@Autowired
private RedisUtil redisUtil;
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
URI uri = serverHttpRequest.getURI();
// 不是登录请求,直接向下执行
if (!StringUtils.containsIgnoreCase(uri.getPath(), AUTH_URL)) {
return chain.filter(exchange);
}
if (HttpMethod.POST.matches(serverHttpRequest.getMethodValue())) {
String bodyStr = ServletHttpHelper.resolveBodyFromRequest(serverHttpRequest);
try {
JSONObject bodyJson = JSONObject.parseObject(bodyStr);
String username = String.valueOf(bodyJson.get("username"));
String password = String.valueOf(bodyJson.get("password"));
String code = String.valueOf(bodyJson.get("code"));
String uuid = String.valueOf(bodyJson.get("uuid"));
// 登陆校验
loginCheckPre(username, password, code, uuid);
} catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
AjaxResult ajaxResult = AjaxResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
String msg = JSON.toJSONString(ajaxResult);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(msg.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
DataBuffer bodyDataBuffer = ServletHttpHelper.transferBodyStrToDataBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
return chain.filter(exchange.mutate().request(request).build());
}
return chain.filter(exchange);
};
}
/**
* 登陆前置检查
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 随机数
* @throws Exception
*/
private void loginCheckPre(String username, String password, String code, String uuid) throws Exception {
// 非空
if (StringUtils.isBlank(username)) {
throw new LoginException("用户名不能为空");
}
if (StringUtils.isBlank(password)) {
throw new LoginException("密码不能为空");
}
if (StringUtils.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
if (StringUtils.isBlank(uuid)) {
throw new ValidateCodeException("验证码不合法");
}
// 验证码校验
String verifyKey = CommonConstants.CAPTCHA_CODE_KEY + uuid;
String saveCode = redisUtil.get(verifyKey);
redisUtil.delete(verifyKey);
if (StringUtils.isBlank(saveCode)) {
throw new ValidateCodeException("验证码已过期");
}
if (!code.equalsIgnoreCase(saveCode)) {
throw new ValidateCodeException("验证码错误");
}
}
}
yml中使用自定义过滤器:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router
lower-case-service-id: true # 服务名小写
# 路由(如果使用动态路由方式,不要在配置文件中配置路由)
routes:
# 认证中心
- id: nevims-app-auth
uri: lb://nevims-auth-service
predicates:
- Path=/nevims/api-auth/**
filters:
# 验证码处理
- StripPrefix=2
- LoginCaptchaFilter
3.支持yml参数化配置
如果我们想支持在yml文件中给自定义过滤器配置特定参数,我们可以给自定义过滤器指定Config:
package com.faw.btims.gateway.filter;
import com.faw.btims.gateway.utils.RedisUtil;
import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import java.util.Arrays;
import java.util.List;
/**
* FileName: LoginCaptchaFilterGatewayFilterFactory
* Author: TP
* Description:自定义过滤器
*/
public class LoginCaptchaFilterGatewayFilterFactory extends AbstractGatewayFilterFactory<LoginCaptchaFilterGatewayFilterFactory.Config> {
private RedisUtil redisUtil;
//重写构造函数,指定Config对象为我们自定义的静态内部类Config
public LoginCaptchaFilterGatewayFilterFactory(RedisUtil redisUtil) {
super(Config.class);
this.redisUtil = redisUtil;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled", "authUrl");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
System.out.println(config.enabled);
System.out.println(config.authUrl);
//TODO 业务处理
return chain.filter(exchange);
};
}
/**
* 自定义的config类,用来设置传入的参数
*/
@Data
public static class Config {
// 是否启用
private boolean enabled;
// 白名单
private String authUrl;
}
}
注册bean:
@Configuration
public class FilterConfig {
@Autowired
private RedisUtil redisUtil;
@Bean
public LoginCaptchaFilterGatewayFilterFactory helloWorld(){
return new LoginCaptchaFilterGatewayFilterFactory(redisUtil);
}
}
yml中配置过滤器并指定参数:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router
lower-case-service-id: true # 服务名小写
# 路由(如果使用动态路由方式,不要在配置文件中配置路由)
routes:
# 认证中心
- id: nevims-app-auth
uri: lb://nevims-auth-service
predicates:
- Path=/nevims/api-auth/**
filters:
# 验证码处理
- StripPrefix=2
- name: LoginCaptchaFilter
args:
enabled: true
authUrl: '/auth/login'
访问配置的路由路径后,控制输出:
true
/auth/login
注意:
springboot约定过滤器的前缀为配置的name,而后面最好统一都是GatewayFilterFactory
- 我们在yml中配置的过滤器名称,如果你的自定义过滤器类名为:XxxGatewayFilterFactory,则名字为Xxx
- 而如果你的自定义过滤器类名中不以GatewayFilterFactory结尾,例如只为Xxx,则直接写Xxx即可。
但推荐按标准写法:自定义GatewayFilterFactory为:XxxGatewayFilterFactory,
yml中配置name为Xxx
另:如果我们的参数只需要一个,我们可以在yml中配置时直接写成过:滤器名字=参数值即可,例如上面的StripPrefix=2
4.为自定义GatewayFilterFactory设置order
我们通过这种工厂创建出来的过滤器是没有指定order的,会被默认设置为是0,配置在yml文件中,则按照它书写的顺序来执行。
如果想要在代码中设置好它的顺序,工厂的apply方法需要做一些修改,例如:
@Override
public GatewayFilter apply(Config config) {
return new InnerFilter(config);
}
/**
* 创建一个内部类,来实现2个接口,指定顺序
*/
private class InnerFilter implements GatewayFilter, Ordered {
private Config config;
InnerFilter(Config config) {
this.config = config;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// TODO 业务处理
// 我们也可以在then方法里的,相当于aop中的后置通知,进行增强(可选操作)
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println(" post 自定义过滤器工厂: " + this.getClass().getSimpleName());
}));
}
@Override
public int getOrder() {
return -100;
}
}