今天我们来学习下 Srping Cloud Gateway
的全局过滤器 GloableFilter
。
全局过滤器
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。
GlobalFilter 接口
package org.springframework.cloud.gateway.filter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
我们单独定义也挺简单的,只需要实现GlobalFilter
, Ordered
这两个接口就可以了。
官网上给的案例如下:
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
下面我们观察一个自定义的全局过滤器案例:
/**
* 网关鉴权
*
* @author ezhang
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
private final static long EXPIRE_TIME = Constants.TOKEN_EXPIRE * 60;
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Resource(name = "stringRedisTemplate")
private ValueOperations<String, String> sops;
@Value("${ADMIN-IP}")
private String adminIp;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String url = exchange.getRequest().getURI().getPath();
String userIpaddr = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites()))
{
return chain.filter(exchange);
}
String token = getToken(exchange.getRequest());
if (StringUtils.isBlank(token))
{
return setUnauthorizedResponse(exchange, "令牌不能为空", com.erow.base.common.constant.HttpStatus.ERROR);
}
String userStr = sops.get(getTokenKey(token));
if (StringUtils.isNull(userStr))
{
return setUnauthorizedResponse(exchange, "登录状态已过期",com.erow.base.common.constant.HttpStatus.FORBIDDEN);
}
JSONObject obj = JSONObject.parseObject(userStr);
String userid = obj.getString("userid");
String username = obj.getString("username");
String rootin = obj.getString("rootin");
String passwordUpdateDate = obj.getString("passwordUpdateDate");
String roleIds = obj.getString("roleIds");
if (StringUtils.isBlank(userid) || StringUtils.isBlank(username))
{
return setUnauthorizedResponse(exchange, "令牌验证失败",com.erow.base.common.constant.HttpStatus.ERROR);
}
//校验用户密码是否已过期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Date parse = null;
try {
parse = sdf.parse(passwordUpdateDate);
int differentDays = DateUtils.daysBetween(parse,new Date());
//修改密码接口放行
if(UserConstants.MAXEXPIRED<= differentDays && !url.equals("/auth/updateUser")){
return setUnauthorizedResponse(exchange, "您的密码已过期,请重新修改密码",com.erow.base.common.constant.HttpStatus.PWD_EXPIRED);
}
} catch (NullPointerException e) {
//修改密码接口放行
if(parse==null && !url.equals("/auth/updateUser")){
return setUnauthorizedResponse(exchange, "首次登录,请先修改密码!",com.erow.base.common.constant.HttpStatus.PWD_EXPIRED);
}
}catch (Exception e) {
e.printStackTrace();
}
// 设置过期时间
redisService.expire(getTokenKey(token), EXPIRE_TIME);
// 设置用户信息到请求
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(CacheConstants.DETAILS_USER_ID, userid)
.header(CacheConstants.DETAILS_USERNAME, ServletUtils.urlEncode(username)).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, String msg,String code)
{
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
return bufferFactory.wrap(JSON.toJSONBytes(AjaxResult.error(code,msg)));
}));
}
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
private String getLoginUserKey(String rootin,String userId)
{
return rootin+"_"+CacheConstants.LOGIN_USER_KEY + userId;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request)
{
String token = request.getHeaders().getFirst(CacheConstants.HEADER);
if (StringUtils.isNotEmpty(token) && token.startsWith(CacheConstants.TOKEN_PREFIX))
{
token = token.replace(CacheConstants.TOKEN_PREFIX, "");
}
return token;
}
@Override
public int getOrder()
{
return -200;
}
}
上面的自定义全局过滤器中,我们用来统一处理了一些业务需求,比如 token 令牌验证、白名单跳过等等。
@Override
public int getOrder()
{
return -200;
}
这部分代码定义过滤器执行的优先级
order 越大,优先级越低
局部过滤器
全局过滤器作用于所有的路由,不需要单独配置。局部过滤器就不一样了,需要单独配置。
局部过滤器步骤:
-
需要继承
AbstractGatewayFilterFactory
,覆盖相关的方法。 -
在配置文件中进行配置,如果不配置则不启用此过滤器规则。
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url)) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return exchange.getResponse().writeWith(
Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes("请求地址不允许访问"))));
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter()
{
super(Config.class);
}
public static class Config {
private List<String> blacklistUrl;
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
public boolean matchBlacklist(String url) {
return blacklistUrlPattern.isEmpty() ? false : blacklistUrlPattern.stream().filter(p -> p.matcher(url).find()).findAny().isPresent();
}
public List<String> getBlacklistUrl() {
return blacklistUrl;
}
public void setBlacklistUrl(List<String> blacklistUrl) {
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -> {
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\*\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
配置文件中配置:
spring:
cloud:
gateway:
routes:
# 系统模块
- id: cloud-system
uri: lb://cloud-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: BlackListUrlFilter
args:
blacklistUrl:
- /user/list
这样,我们就定义好了一个局部黑名单过滤器。访问 /user/list
地址时会返回:请求地址不允许访问。
白名单配置
其实在上面的 AuthFilter
中已经处理了白名单地址,就是不需要进行校验的地址。
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
ignoreWhite
就是直接读取 yml 中的自定义配置。
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "ignore")
public class IgnoreWhiteProperties
{
/**
* 放行白名单配置,网关不校验此处的白名单
*/
private List<String> whites = new ArrayList<>();
public List<String> getWhites()
{
return whites;
}
public void setWhites(List<String> whites)
{
this.whites = whites;
}
}
yml 配置如下
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /*/v2/api-docs
包含这些地址的 URL 就不需要校验 token 了,直接放行。