Gateway网关
Categories:
什么是网关
网关负责路由请求、负载均衡、安全认证、流量控制、监控和日志记录等任务。
我的理解是, 网关是请求真正进入服务前的过滤, 管理, 控制(重定向), 可以理解为保安大哥
网关的结构
-
路由 网关的基本模块, 由断言和过滤器组成
-
断言 匹配规则, 当请求路径匹配时允许路由到相关服务 实现一组匹配规则,让请求找到对应的 Route 进行处理
-
过滤器 响应式的过滤器链, 实现请求的过滤
- 全局过滤器 全局过滤器作用于所有的路由,不需要单独配置
-
客户端发起请求, 由 Gateway Handler Mapping
接收, 这里执行断言并路由到 Gateway Web Handler
这里做过滤链, 过滤请求, 最后传递给服务
过滤器可以在执行前和执行后回调
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
了解: 响应式编程
过滤器
过滤器是由 Servlet
容器管理的, 也就是说初始化
实践: Spring Cloud Gateway 全局过滤器
网关选型
SpringCloudGateway
这是目前比较新的选择模板工程基于
pmhub
新建自定义的全局过滤器
全局过滤器需要实现 GlobalFilter
过滤器需要确定执行顺序, 实现Order
接口
@Component
public class AuthFilter implements GlobalFilter, Ordered
该过滤器实现如下功能
- 请求耗时记录
- 白名单过滤
- Token鉴权
- 日志输出
请求耗时记录:
全局过滤器的优先级较高, 因此把请求进入过滤器的时间作为起点, 业务执行完毕执行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
@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("我是美丽分割线: ###################################################");
}
}
}
addHeader
和 removeHeader
其实没什么说的, 就是重新构造了个请求, 方便后续服务直接拿信息
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();
}