目录
2、自定义过滤器-TokenAuthGatewayFilterFactory
3、完善TokenAuthGatewayFilterFactory的功能
续前篇,介绍了gateway中实现了动态路由转发功能以后,本篇将介绍何在spring gateway中实现鉴权的功能。
鉴权目的就是为了安全。仅开放给指定的有权限的合适的人资源。网关在提供统一的路由解析的同时,会提供统一和token认证,统一的加解密,统一的身份认证等功能。
一般作为网关,需要转发的服务较多,在服务级权限分类上,可能有这么几种类理:
- 直接放行,比如静态资源。当然这种一般建议直接在nginx跳转,减少网关流量。但是还是会有一些比如login等,需要直接放行的接口。
- 需要jwt等TOKEN验证。在请求头中携带token字段,用来验证是否有合适的身份。这个用得比较多,通常会把token存到redis中。
- 需要sign验证。在请求头或url参数中携带sign验证字段。可能采用md5等计算方法。
- 需要jwt验证,同时需要对报文进行加密传输。比如采用rsa\m3等加密。
- 除此之外还有黑名单。需要全局生效
针对上面的需求,我们就可以利用下面这两个过滤器进行分类实现:
GlobalFilter
全局过滤器GatewayFilter
将应用到单个路由或者同一个分组中的路由上
一、跨域安全设置
如果生产环境限定域名的,可以配置。如果前后端分离的,一般要设置允许所有的网站访问。
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
二、GlobalFilter实现全局的过滤与拦截。
利用GlobalFilter实现全局过滤-黑名单功能,新建一个类,继承GlobalFilter就好了:
java
package com.iuyun.gateway.filter;
import com.iuyun.gateway.services.IpService;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.Objects;
/**
* 这是自定义的全局过滤器
* 1、 黑名单禁止访问
* 2、 ...
*/
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Resource
IpService ipService;
@Override
public int getOrder() {
// TODO Auto-generated method stub
return -1; // 数字越小,优先级越高。定义-1为最高优先级
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
System.out.println("MyGlobalFilter: uri:" + request.getURI() );
// 获取请求ip
String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
// 1 调用服务,检查是否在黑名单池内.如果是黑名单,则中断。
if (ipService.isIpBlocked(ip)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
// 2 执行其他全局服务
// 3 放行
return chain.filter(exchange);
}
}
调用到的方法(模拟):
java
@Service
public class IpService {
/**
* 根据IP地址查询数据库中的黑白名单列表,判断是否被阻止
* */
public boolean isIpBlocked(String ipAddress) {
System.out.println("ip:"+ipAddress);
// 将ip去redis或者数据库中查找,是否在黑名单内。
// return count >0; // 只要出现条数大于0,则表示在默名单内
// 这里面是模拟,写死了
return ipAddress.equals("192.168.2.3");
}
}
测试:我们从不同IP来访问不同的服务,可以看到都会被执行:
当IP在黑名单内时,会返回403:
三、GatewayFilter单个服务过滤器
将应用到单个路由或者同一个分组中的路由上。
1、原理-官方内置过滤器
我们先看一下官方的示例:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue
表示使用过滤器AddResponseHeader,=后面的是参数,它接收一个名称和一个正则表式。
那我们去看看它是怎么实现的:
看了一下其他的filterFactory,都差不多,简单总结一下:
1、命名:PrefixPathGatewayFilterFactory => XXXXGatewayFilterFactory
2、继承抽象类 AbstractGatewayFilterFactory<PrefixPathGatewayFilterFactory.Config>
3、重写方法GatewayFilter apply(Config config)
我们如果照抄一份,是不是就可以实现自定义过滤器了呢?下面我就来试一下。
2、自定义过滤器-TokenAuthGatewayFilterFactory
1)编写一个GatewayFilterFactory(复制AddResponseHeaderFilterFactory的内容,改个名)
java
@Component
public class TokenAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
exchange.getResponse().getHeaders().add(config.getName(), value);
System.out.println("config.getName():" + config.getName());
System.out.println("value:" + value);
return chain.filter(exchange);
}
@Override
public String toString() {
return filterToStringCreator(TokenAuthGatewayFilterFactory.this)
.append(config.getName(), config.getValue()).toString();
}
};
}
}
2)application配置
3)重启,看一下测试效果
先走了全局过滤器,然后这个se-a服务走了这个filter,打印了变量。
这就证明了可以这样实现自定义过滤器,那么,我们在gateway中做任何的校验就方便了。且predicates和filters是可以使用多个进行组合的:
filters: - StripPrefix=1 - TokenAuth=jwt,Hello - AddResponseHeader=jwt,Hello1
这样就使用了三个filter。
3、完善TokenAuthGatewayFilterFactory的功能
(这个过程就根据自己的需要进行编写吧,比如,采用RSA加密,sm3加密,验证JWT等)
这里我模拟一个需求如下:
每次请求必须在请求头或url参数中传递一个token字段。
如果没有直接拒绝请求.如果token不对,返回401。
只有token正确,才放行,并在请求头添加一个userId字段传递给被调用的微服务。
要想实现直接拒绝不响应,可以使用predicatie,不符合条件就返回404了:
- id: service-a uri: lb://service-a # lb:服务名称。表示调用nacos注册的服务名称为service-b的服务 predicates: - Path=/se-a/** - Header=token,\d+ filters: - StripPrefix=1 - TokenAuth=jwt,Hello
但是这样并不友好,有统一返回值是现在流行的做法。且万一在prdicates中忘记加这个Header了,在filter中就因为没有token字段而出错。所以为了防止出错在filter还是要再进行一次token是否为空的校验,那么就干脆都在filter中实现吧。
java
@Component
public class TokenAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); // config.getValue() config.getName()
String headerToken = exchange.getRequest().getHeaders().getFirst("token");
String urlToken = exchange.getRequest().getQueryParams().getFirst("token");
// 优先使用header中的token,其次是url中的token,如果没有则继续为null
String reqToken =StringUtils.isBlank(headerToken)? urlToken:headerToken;
// 检查token是否存在且正确,一般都需要去调auth服务,从数据库或redis拿到密码并校验
String userId = TokenCheck.JwtCheck(reqToken);
if(StringUtils.isBlank(reqToken) || StringUtils.isBlank(userId)){
// 返回状态码401表示未授权
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 运行到这里,表示token计算正确。将UserID添加到header进行传递
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.add("userId",userId)).build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public String toString() {
return filterToStringCreator(TokenAuthGatewayFilterFactory.this)
.append(config.getName(), config.getValue()).toString();
}
};
}
}
java
@Component
public class TokenCheck {
public static String JwtCheck(String token){
// 去redis获取是否存在这个token,如果不存在,则表示未登陆或登陆已过期。
// 如果没存到redis,则这里要粗数据库调user表,查询用户的密码,并调用jwt的方法计算密码是否正确
// 这里做模拟,先写死。如果正确,返回一个userId =
if(!StringUtils.isBlank(token) && !token.equals("jwt11234")){
return null;
}else{
// 这里写死,返回一个userID
return "admin";
}
}
}
4、每一个服务编写一个或多个过滤器,实现鉴权的需要
也可以多个服务使用同一个过滤器。
四、总结
由于本篇重点是讲gateway中如何实现鉴权,所以并未展开介绍。在上面的示例中,我们并没有真的实现jwt验证等,只是做了一个固定值的返回。后面我们将继续介绍实现jwt的验证。