目前我们项目的架构图:
从上图中可以看到,Zuul是我们整个系统的入口。当我们有参数校验的需求时,我们就可以利用Zuul的Pre过滤器,进行参数的校验。例如我现在希望请求都一律带上token参数,否则拒绝请求。在项目中创建一个filter包,在该包中新建一个TokenFilter劳累并继承ZuulFilter,代码如下:
package org.zero.springcloud.apigateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @program: api-gateway
* @description: token过滤器
* @author: 01
* @create: 2018-08-25 17:03
**/
@Component
public class TokenFilter extends ZuulFilter {
@Override
public String filterType() {
// 声明过滤器的类型为Pre
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 将这个过滤器的优先级放在 PRE_DECORATION_FILTER_ORDER 之前,数字越小优先级越高
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
// 开启这个过滤器
return true;
}
/**
* 这个方法用于自定义过滤器的处理代码
*
* @return Object
* @throws ZuulException ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
// 从上下文中拿到请求对象
HttpServletRequest request = requestContext.getRequest();
// 拿出参数里的token
String token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
// 验证失败
requestContext.setSendZuulResponse(false);
// 返回401权限不通过
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
重启项目,我们来访问一个接口,不带上token参数,看看是否会返回401。如下:
带上token参数再测试一下,请求成功:
从以上的示例中,可以看到利用Pre可以对请求进行一些预处理。如果希望在请求处理完成后,对返回的数据进行处理的话。就需要使用的Post过滤器,例如我们要在http返回头中,加上一个自定义的X-Foo
属性。代码如下:
package org.zero.springcloud.apigateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @program: api-gateway
* @description:
* @author: 01
* @create: 2018-08-25 17:10
**/
@Component
public class AddResponseHeaderFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
response.setHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
重启项目,同样访问之前那个接口,测试结果如下:
Zuul充当API网关的角色,所有的请求都经过它,所以很适合在其之上对API做限流保护,防止网络×××。需要注意的是,用于限流的过滤器应该在请求被转发之前调用,常见的限流算法有计数器、漏铜和令×××桶算法。
令×××桶算法示意图:
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令×××桶算法(Token Bucket)来完成限流,非常易于使用。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率,它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到。
我们来创建一个过滤器,简单使用一下这个RateLimiter。代码如下:
package org.zero.springcloud.apigateway.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.exception.RateLimiterException;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVLET_DETECTION_FILTER_ORDER;
/**
* @program: api-gateway
* @description: 限流过滤器
* @author: 01
* @create: 2018-08-25 21:04
**/
@Component
public class RateLimiterFilter extends ZuulFilter {
/**
* 每秒钟放入100个令×××
*/
private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
@Override
public String filterType() {
// 限流肯定是得在Pre类型的过滤器里做
return PRE_TYPE;
}
@Override
public int filterOrder() {
// 设置过滤器的优先级为最高
return SERVLET_DETECTION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
// 尝试从令×××桶中获取令×××
if (!RATE_LIMITER.tryAcquire()) {
// 获取失败抛出异常,或做其他处理
throw new RateLimiterException();
}
return null;
}
}
除了这个RateLimiter之外,GitHub上也有一些开源的实现。我这里发现了一个还不错的,地址如下:
https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
以上我们演示了pre、post过滤器的简单使用,以及在Zuul上做限流,接下来我们看看如何通过Zuul实现鉴权。通常来说,我们鉴权的对象往往都是用户,我这里已经事先准备好了用户服务以及相关接口。
需求,利用Zuul实现如下功能:
/**
* /buyer/order/create 只能买家访问 (cookie里有openid)
* /buyer/order/finish 只能卖家访问 (cookie里有token,并且redis存储了session数据)
* /buyer/product/list 都可以访问
*/
因为判断用户角色权限的时候,需要通过cookie和redis里缓存的数据进行判断,所以修改配置文件如下:
将之前做实验的所有过滤器都注释掉,然后新建一个AuthBuyerFilter过滤器,用于拦截订单创建的请求。代码如下:
package org.zero.springcloud.apigateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.utils.CookieUtil;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
/**
* @program: api-gateway
* @description: 买家权限过滤器
* @author: 01
* @create: 2018-08-25 17:03
**/
@Component
public class AuthBuyerFilter extends ZuulFilter {
private static final String ORDER_CREATE = "/order/buyer/order/create";
@Override
public String filterType() {
// 声明过滤器的类型为Pre
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 将这个过滤器的优先级放在 PRE_DECORATION_FILTER_ORDER 之前
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
// 从上下文中拿到请求对象
HttpServletRequest request = requestContext.getRequest();
// 如果访问的是 ORDER_CREATE 则进行拦截,否则不进行拦截
return ORDER_CREATE.equals(request.getRequestURI());
}
/**
* 这个方法用于自定义过滤器的处理代码
*
* @return Object
* @throws ZuulException ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
// 从上下文中拿到请求对象
HttpServletRequest request = requestContext.getRequest();
// /buyer/order/create 只能买家访问 (cookie里有openid)
Cookie cookie = CookieUtil.get(request, "openid");
if (cookie == null || StringUtils.isBlank(cookie.getValue())) {
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
接着再新建一个AuthSellerFilter过滤器,用于拦截订单完结的请求。代码如下:
package org.zero.springcloud.apigateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.constant.RedisConstant;
import org.zero.springcloud.apigateway.utils.CookieUtil;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
/**
* @program: api-gateway
* @description: 卖家权限过滤器
* @author: 01
* @create: 2018-08-25 17:03
**/
@Component
public class AuthSellerFilter extends ZuulFilter {
private final StringRedisTemplate redisTemplate;
private static final String ORDER_FINISH = "/order/buyer/order/finish";
@Autowired
public AuthSellerFilter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public String filterType() {
// 声明过滤器的类型为Pre
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 将这个过滤器的优先级放在 PRE_DECORATION_FILTER_ORDER 之前
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
// 从上下文中拿到请求对象
HttpServletRequest request = requestContext.getRequest();
// 如果访问的是 ORDER_FINISH 则进行拦截,否则不进行拦截
return ORDER_FINISH.equals(request.getRequestURI());
}
/**
* 这个方法用于自定义过滤器的处理代码
*
* @return Object
* @throws ZuulException ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
// 从上下文中拿到请求对象
HttpServletRequest request = requestContext.getRequest();
// /buyer/order/finish 只能卖家访问 (cookie里有token,并且redis存储了session数据)
if (ORDER_FINISH.equals(request.getRequestURI())) {
Cookie cookie = CookieUtil.get(request, "token");
if (cookie == null ||
StringUtils.isBlank(cookie.getValue()) ||
StringUtils.isNotBlank(redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())))) {
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
}
return null;
}
}
额外话题:
现在我们的项目基本都是前后端分离的,前端通过ajax来请求后端接口。由于浏览器的同源策略,所以会出现跨域的问题。而在微服务架构中,我们可以在网关上统一解决跨域的问题。
在Zuul里增加CorsFilter过滤器的配置类即可。代码如下:
package org.zero.springcloud.apigateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @program: api-gateway
* @description: 跨域配置
* @author: 01
* @create: 2018-08-27 23:02
**/
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许cookie跨域
corsConfiguration.setAllowCredentials(true);
// 允许任何域名使用
corsConfiguration.addAllowedOrigin("*");
// 允许任何头
corsConfiguration.addAllowedHeader("*");
// 允许任何方法(post、get等)
corsConfiguration.addAllowedMethod("*");
// 设置跨域缓存时间,单位为秒
corsConfiguration.setMaxAge(300L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}