Gateway网关

网关是一个位于微服务架构前端的组件,它充当了所有微服务的入口

什么是网关

网关负责路由请求、负载均衡、安全认证、流量控制、监控和日志记录等任务。

我的理解是, 网关是请求真正进入服务前的过滤, 管理, 控制(重定向), 可以理解为保安大哥

网关的结构

  • 路由 网关的基本模块, 由断言和过滤器组成

    • 断言 匹配规则, 当请求路径匹配时允许路由到相关服务 实现一组匹配规则,让请求找到对应的 Route 进行处理

    • 过滤器 响应式的过滤器链, 实现请求的过滤

      • 全局过滤器 全局过滤器作用于所有的路由,不需要单独配置

image.png

客户端发起请求, 由 Gateway Handler Mapping 接收, 这里执行断言并路由到 Gateway Web Handler 这里做过滤链, 过滤请求, 最后传递给服务 过滤器可以在执行前和执行后回调

在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;

在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

了解: 响应式编程

过滤器

过滤器是由 Servlet 容器管理的, 也就是说初始化

image.png

实践: Spring Cloud Gateway 全局过滤器

网关选型SpringCloudGateway 这是目前比较新的选择

模板工程基于 pmhub

新建自定义的全局过滤器

全局过滤器需要实现 GlobalFilter 过滤器需要确定执行顺序, 实现Order 接口

@Component
public class AuthFilter implements GlobalFilter, Ordered

该过滤器实现如下功能

  1. 请求耗时记录
  2. 白名单过滤
  3. Token鉴权
  4. 日志输出

请求耗时记录:

全局过滤器的优先级较高, 因此把请求进入过滤器的时间作为起点, 业务执行完毕执行post 业务逻辑. 进行简单的时间相减

白名单过滤:

nacos 配置白名单, 有些路径不需要严格的鉴权, 比如首页. 所有人发请求都能通过, 就直接跳过完事

Token鉴权:

作为全局过滤器, 当用户请求进入应当进行身份鉴权, 过滤无效的请求 从请求头里拿 Token , 跟缓存做匹配即可

Redis 策略是到期销毁, 不做更新

日志输出: 在过滤链回调后执行, 代表服务执行完成 结束时间减去开始时间就行

为了方便解释, 我们一项项实现

请求耗时:

 @Component
public class AuthFilter implements GlobalFilter, Ordered{
	//常量, 记录开始访问的时间
	private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间

	//补上日志实例
	private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
	
//过滤器接口方法, 这个必须实现
	@Override  
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	//token和白名单稍后实现
	
	//在上下文加上开始时间
	exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

	//等待链执行完成回调
	return chain.filter(exchange)
	//链式调用then, 使用Mono.fromRunnable方法顺序执行日志输出
	.then(Mono.fromRunnable{})
		//日志输出的逻辑
		// 记录接口访问日志  
		Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
		if (beginVisitTime != null){
			logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");
			log.info("访问接口信息: {}", logData);
		}
	}
}

白名单过滤

 @Component
public class AuthFilter implements GlobalFilter, Ordered{
	//常量, 记录开始访问的时间
	private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间

	//补上日志
	private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
	
//过滤器接口方法
	@Override  
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	//白名单
	//首先拿到请求路径
	//上下文拿请求
	ServerHttpRequest request = exchange.getRequest();
	//请求拿请求路径, 先拿URI, 再去掉首部
	String url = request.getURI().getPath();
	
	// 跳过不需要验证的路径, 直接进下一级
	if (StringUtils.matches(url, ignoreWhite.getWhites())) {  
	    return chain.filter(exchange);  
	}
	
	//在上下文加上开始时间
	exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

	//等待链执行完成回调
	return chain.filter(exchange)
	//链式调用then, 使用Mono.fromRunnable方法顺序执行日志输出
	.then(Mono.fromRunnable{})
		//日志输出的逻辑
		// 记录接口访问日志  
		Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
		if (beginVisitTime != null){
			logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");
			log.info("访问接口信息: {}", logData);
		}
	}
}

token鉴权: 选型 Jwt

下图是流程, 我们将鉴权的业务从服务层分离移至网关进行, 优化性能

鉴权(认证)是在网关层进行的,而 请求并没有进入到 MVC 控制器中, 也就没有触发拦截器

了解: SpringMVC

image.png

 @Component
public class AuthFilter implements GlobalFilter, Ordered{
	//常量, 记录开始访问的时间
	private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间

	//补上日志
	private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
	
	//补上redis
	@Autowired  
	private RedisService redisService;
//过滤器接口方法
	@Override  
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	//白名单
	//首先拿到请求路径
	//上下文拿请求
	ServerHttpRequest request = exchange.getRequest();
	//请求拿请求路径, 先拿URI, 再去掉首部
	String url = request.getURI().getPath();
	
	// 跳过不需要验证的路径, 直接进下一级, 返回链的执行结果
	if (StringUtils.matches(url, ignoreWhite.getWhites())) {  
	    return chain.filter(exchange);  
	}
	
	//剩下的请求过筛子
	//从请求拿token
	String token = getToken(request);
	if (StringUtils.isEmpty(token)) {  
    return unauthorizedResponse(exchange, "令牌不能为空");  
	}
	
	//拿JWT的声明, 这里写了一个字符串工具拿声明, 令牌秘钥就在工具里
	Claims claims = JwtUtils.parseToken(token);
	if (claims == null) {  
	    return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");  
	}
	
	//从声明拿用户ID
	String userkey = JwtUtils.getUserKey(claims);  
	//从缓存匹配用户ID的token, 这里getTokenKey格式化, 就加了个前缀
	boolean islogin = redisService.hasKey(getTokenKey(userkey));
	if (!islogin) {  
    return unauthorizedResponse(exchange, "登录状态已过期");  
	}
	
	//最后从声明拿用户详细信息
	String userid = JwtUtils.getUserId(claims);  
	String username = JwtUtils.getUserName(claims);  
	if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {  
	    return unauthorizedResponse(exchange, "令牌验证失败");  
	}
	
	//重新设置请求, 鉴权完毕
	//设置用户信息到请求  
	addHeader(mutate, SecurityConstants.USER_KEY, userkey);  
	addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);  
	addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);  
	// 内部请求来源参数清除(防止网关携带内部请求标识,造成系统安全风险)  
	removeHeader(mutate, SecurityConstants.FROM_SOURCE);
	//在上下文加上开始时间
	exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

	//等待链执行完成回调
	return chain.filter(exchange)
	//链式调用then, 使用Mono.fromRunnable方法顺序执行日志输出
	.then(Mono.fromRunnable{})
		//日志输出的逻辑
		// 记录接口访问日志  
		Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);  
		if (beginVisitTime != null) {  
		    URI uri = exchange.getRequest().getURI();  
		    Map<String, Object> logData = new HashMap<>();  
		    logData.put("host", uri.getHost());  
		    logData.put("port", uri.getPort());  
		    logData.put("path", uri.getPath());  
		    logData.put("query", uri.getRawQuery());  
		    logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");  
  
		    log.info("访问接口信息: {}", logData);  
		    log.info("我是美丽分割线: ###################################################");  
		}
	}
}

addHeaderremoveHeader 其实没什么说的, 就是重新构造了个请求, 方便后续服务直接拿信息

private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {  
    if (value == null) {  
        return;  
    }  
    String valueStr = value.toString();  
    String valueEncode = ServletUtils.urlEncode(valueStr);  
    mutate.header(name, valueEncode);  
}  
  
private void removeHeader(ServerHttpRequest.Builder mutate, String name) {  
    mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();  
}