网关概述
简单的理解,网关主要功能就是过滤和路由转发,统一了对后端服务的访问。网关基于ZuulServlet,定义了一组ZuulFilter过滤器实现各类拦截逻辑,ZuulFilter定义了pre,route,post,err四种类型。ZuulServlet的service方法源码如下:
// ZuulServlet.java
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
// pre阶段
preRoute();
} catch (ZuulException e) {
// error
error(e);
// 报错以后,先执行error,然后执行post
postRoute();
return;
}
try {
// route 路由阶段
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
ZuulServlet在启动时,默认配置许多ZuulFilter,定义RequestContext上下文保存各个过滤器对请求参数的操作。以下主要讲解下PreDecorationFilter和RibbonRoutingFilter的功能。
- PreDecorationFilter,把ZuulProperties(zuul配置文件映射的值)转换到RequestContext,默认屏蔽了authenrization,cookie等header向下游请求传递。
- RibbonRoutingFilter,使用httpclient通过服务注册中心负载均衡的转发匹配的服务。
zuul网关的应用
zuul网关所有服务都是通过ZuulFilter来实现的,包括路由转发,认证授权,限流,降级,防止用户重复提交等功能。以下举例说明常见业务的实现。
路由转发
在zuul的路由转发功能,由如下过滤器具体实现:
- SimpleHostRoutingFilter,直接转换host地址。
- SendForwardFilter,本地url跳转。
- RibbonRoutingFilter,基于服务注册与发现,动态的路由转发。
路由常见的使用情况如下:
- zuul天然的与服务注册中心(eureka服务注册与发现)集成,通过ribbon负载均衡到映射的服务。
- 在集成老的业务系统到网关时,由于没有使用服务注册与发现组件,需要直接配置映射地址。这时,zuul可以使用基于数据库的数据,实现动态路由。具体实现可以参考阿里大神写的springcloud—-Zuul 动态路由。大致两方面的内容,容器启动时,路由数据的动态获取,以及运行时,基于Spring 的事件发布机制动态(可以参考Spring事件发布机制-Tomcat教你如何玩)刷新路由信息。
动态路由的部分实现代码如下:
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
private ZuulProperties properties;
public CustomRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.properties = properties;
}
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
//从application.properties中加载路由信息
routesMap.putAll(super.locateRoutes());
//从db中加载路由信息
routesMap.putAll(locateRoutesFromDB());
//优化一下配置
LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
// Prepend with slash if not already present.
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB() {
Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
// TODO 从数据库获取配置
return routes;
}
}
运行时,基于Spring事件发布机制动态刷新的代码如下:
@Service
public class RefreshRouteService {
@Autowired
private ApplicationEventPublisher publisher;
@Autowired
private RouteLocator routeLocator;
/**
* 刷新路由
*/
public void refreshRoute() {
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}
身份认证
在微服务中,一般基于OAuth2协议进行认证和授权,客户端向授权服务进行身份认证,认证通过后获取访问授权码(access token);然后,携带访问授权码(access token)访问指定资源服务器,资源授权通过以后就可以访问到请求的资源。此时,把资源授权的逻辑统一在网关处理,就避免在各个下游服务重复的做授权处理。基于spring cloud security实现 oauth2协议的具体流程实现可以参考玩转Spring Cloud Security OAuth2资源授权动态权限扩展。
这里,重点讲解下,在网关授权成功以后,只能在网关服务中,通过认证上下文获取到当前认证用户的信息,网关转发的各个下游服务是获取不到当前登录用户信息的。这时有两种思路实现:
- 在认证的access token中,存储当前用户信息,只要下游的微服务拿到access token做一个解析就可以拿到认证用户信息。
- 首先,在认证服务器创建一个可以根据access token获取当前认证用户的api。网关把access token转发到下游微服务,在下游的微服务中,实现一个servlet 过滤器,根据access token从认证服务器的api获取用户信息,并存在一个用户上下文中即可。
那么zuul网关该如何转发访问授权码(access token)呢?有两种方法可以实现:
1. 开启屏蔽的header配置,因为,在PreDecorationFilter过滤器中,默认屏蔽了Authenrization转发到下游服务,配置如下:
zuul:
sensitive-headers:
sensitive-headers属性为空,就会屏蔽部分默认的header,其中就包括用于授权的Authenrization头信息。
2. 自定义pre类型的GlobalFilter ,在header中将Authenrization转换access_token然后再转发,GlobalFilter 的run方法代码如下:
// GlobalFilter.java
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String accessToken;
// 把Authenrization转换为accessToken
if (StringUtils.isNotBlank(accessToken = extractHeaderToken(request))) {
ctx.addZuulRequestHeader("access_token", accessToken);
}
return null;
}
服务限流
在微服务中,对于访问非常频繁,超出了服务器的极限,可以在网关对该api进行限流。在并发比较高的场景,例如秒杀场景,可以使用令牌桶法则进行限流。令牌桶法则,可以在调用前计算出当前时间可用的令牌,该特性使其适用于有流量陡增的场景,获取令牌时,如果有则访问成功,反之则失败。示例,使用gava中的令牌桶法则实现类RateLimiter,对映射的api进行限流,代码如下:
public class RequestsLimitFilter extends ZuulFilter implements InitializingBean, ApplicationContextAware {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private ApplicationContext applicationContext;
/**
* 服务限流配置
*/
private List<RequestsLimitConfig> requestsLimitConfigs;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 11;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
try {
if (CollectionUtils.isEmpty(requestsLimitConfigs)) {
log.info("no requests limit configs!");
return null;
}
HttpServletRequest request = ctx.getRequest();
for (RequestsLimitConfig requestsLimitConfig : requestsLimitConfigs) {
// 匹配的请求
String urlPattern;
RateLimiter rateLimiter;
if (StringUtils.isNotEmpty(urlPattern = requestsLimitConfig.urlPattern)
&& antPathMatcher.match(urlPattern, RequestUtils.getRequestUriIfRemovePreServiceId(request))
&& request.getMethod().equalsIgnoreCase(requestsLimitConfig.getLimitMethod())
&& Objects.nonNull(rateLimiter = requestsLimitConfig.rateLimiter)) {
// 使用令牌桶法则,尝试获取令牌
boolean success = rateLimiter.tryAcquire(1, TimeUnit.SECONDS);
if (!success) {
log.warn("limit request!url {},method {}", request.getRequestURI(), request.getMethod());
// 不走路由,RibbonRoutingFilter shouldFilter 判断条件之一
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
ctx.setResponseBody(HttpStatus.TOO_MANY_REQUESTS.toString());
}
}
}
} catch (Exception e) {
String errorMsg = "RequestsLimitFilter error when run!";
log.error(errorMsg, e);
WebUtils.responseOutJson(ctx.getResponse(), JSON.toJSONString(com.kuqi.mall.common.response.Response.builder().code(500).message(errorMsg).build()));
}
return null;
}
@Override
public void afterPropertiesSet() throws Exception {
// spring bean注入时,获取服务限流配置
RequestsLimitService requestsLimitService = applicationContext.getBean(RequestsLimitService.class);
if (Objects.nonNull(requestsLimitService)) {
this.requestsLimitConfigs = requestsLimitService.findRequestsLimitList();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 限流配置
*/
@Data
public static class RequestsLimitConfig implements Serializable {
private static final long serialVersionUID = 3477902563524091219L;
/**
* 拦截url的pattern
*/
private String urlPattern;
/**
* 拦截方法
*/
private String limitMethod;
/***
* 令牌桶过滤器
*/
private RateLimiter rateLimiter;
}
}
用户频率限制
对于类似于下单的场景,如果在秒杀场景,同一个用户频繁的点击下单操作,对服务器性能时一个挑战。这时,可以在网关中,定义匹配服务的用户频率操作限制,防止用户重复提交。示例中,通过redis string类型的setIfAbsent方法,如果不存在对应的key的值,则设置成功,并且其失效时间就标识频繁操作的间隔时间。示例,根据http中,当前用户的id做为标识,对该用户的操作频率做限制,代码如下:
public class RequestsPerUserLimitFilter extends ZuulFilter implements InitializingBean, ApplicationContextAware {
private ApplicationContext applicationContext;
private AntPathMatcher antPathMatcher;
/**
* 限流配置服务
*/
private List<RequestsPerUserLimitConfig> configs;
public RequestsPerUserLimitFilter() {
this.antPathMatcher = new AntPathMatcher();
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 13;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
try {
if (CollectionUtils.isEmpty(configs)) {
log.info("no requests per user limit configs!");
return null;
}
HttpServletRequest request = ctx.getRequest();
for (RequestsPerUserLimitConfig config : configs) {
String urlPattern, limitMethod;
Duration timeout;
if (StringUtils.isEmpty(urlPattern = config.urlPattern)
|| StringUtils.isEmpty(limitMethod = config.limitMethod)
|| Objects.isNull(timeout = config.timeout)) {
log.info("invalid config {} when run!", JSON.toJSONString(config));
continue;
}
// 路径与请求方法匹配
if (antPathMatcher.match(urlPattern, RequestUtils.getRequestUriIfRemovePreServiceId(request))
&& limitMethod.equalsIgnoreCase(request.getMethod())) {
// 用户id+(请求方法+请求路径).hash
String key = RedisConfig.REDIS_PRE + getCurrentUserId(request) + ":" + (request.getMethod() + request.getRequestURI()).hashCode();
try {
// 如果不存在,则设置成功,并且设置了失效时间
RedisTemplate<String, Object> redisTemplate = this.applicationContext.getBean(RedisTemplate.class);
boolean success = redisTemplate.opsForValue().setIfAbsent(key, Thread.currentThread().getId(), timeout);
if (!success) {
log.warn("limit request per user!url {},method {}", request.getRequestURI(), request.getMethod());
// 不走路由,RibbonRoutingFilter shouldFilter 判断条件之一
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
ctx.setResponseBody(HttpStatus.TOO_MANY_REQUESTS.toString());
}
} catch (Exception e) {
log.warn("limit request per user error!", e);
}
}
}
} catch (Exception e) {
String errorMsg = "RequestsPerUserLimitFilter error when run!";
log.error(errorMsg, e);
WebUtils.responseOutJson(ctx.getResponse(), JSON.toJSONString(com.kuqi.mall.common.response.Response.builder().code(500).message(errorMsg).build()));
}
return null;
}
/**
* 获取用户id
*/
private Long getCurrentUserId(HttpServletRequest request) {
String accessToken = RequestUtils.extractToken(request);
UserClient userClient = applicationContext.getBean(UserClient.class);
if (StringUtils.isBlank(accessToken) || Objects.isNull(userClient)) {
return null;
}
User user = userClient.get(accessToken);
if (Objects.isNull(user)) {
return null;
}
return user.getId();
}
@Override
public void afterPropertiesSet() throws Exception {
RequestsLimitService requestsLimitService = this.applicationContext.getBean(RequestsLimitService.class);
if (Objects.nonNull(requestsLimitService)) {
this.configs = requestsLimitService.findRequestsPerUserLimitList();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 用户限流配置
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RequestsPerUserLimitConfig implements Serializable {
private static final long serialVersionUID = 8958979944866916321L;
/**
* 拦截url的pattern
*/
private String urlPattern;
/**
* 拦截方法
*/
private String limitMethod;
/**
* 限流失效时间
*/
private Duration timeout;
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13627.html