一、概述
- sign插件
- sign插件是 soul网关自带的,用来对请求进行签名认证的插件。
- 采用Ak/SK鉴权技术方案。
- 采用鉴权插件,责任链的模式的模式来完成。
- 当鉴权插件开启,并配置所有接口鉴权时候生效。
- waf插件
- waf插件,是网关的用来对流量实现防火墙功能的核心实现。
二、sign插件
3.1 相关配置
-
soul网关引入了soul-spring-boot-starter-plugin-sign
-
在soul-admin中开启sign-plugin插件
-
Admin web端-System Manage-Authentication里添加一个AK/SK 记录
-
然后在sign插件里配置selector和rule
-
像之前一样的请求会报如下错误
-
sign生成方式
需要请求的path、header中的timestamp、以及sk。
生成方式是按照path{path}timestamp{timestamp}version{1.0.0}{sk}的方式形成字符串。如果path为/sofa/findAll,header中的timestamp是1612371127000,sk是8103F45D19464A59A0F339CBC1C4641C,
那么sign=MD5(“path/sofa/findAlltimestamp1612371127000version1.0.08103F45D19464A59A0F339CBC1C4641C”).toUpperCase()=72F2B69CBB8477B10D56EDFA666396B7。请求header中应该有如下4个信息才能过验证:
3.2 源码解读
/**
* The type Default sign service.
*
* @author xiaoyu
*/
@Slf4j
public class DefaultSignService implements SignService {
//可以自定义sign过期时间,单位为分钟
@Value("${soul.sign.delay:5}")
private int delay;
@Override
public Pair<Boolean, String> signVerify(final ServerWebExchange exchange) {
PluginData signData = BaseDataCache.getInstance().obtainPluginData(PluginEnum.SIGN.getName());
//需要配置 sign 插件且 sign 插件是激活状态
if (signData != null && signData.getEnabled()) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
//执行verify方法校验签名
return verify(soulContext, exchange);
}
return Pair.of(Boolean.TRUE, "");
}
private Pair<Boolean, String> verify(final SoulContext soulContext, final ServerWebExchange exchange) {
//任意为空,则失败
if (StringUtils.isBlank(soulContext.getAppKey())
|| StringUtils.isBlank(soulContext.getSign())
|| StringUtils.isBlank(soulContext.getTimestamp())) {
log.error("sign parameters are incomplete,{}", soulContext);
return Pair.of(Boolean.FALSE, Constants.SIGN_PARAMS_ERROR);
}
//获取当前的时间戳
final LocalDateTime start = DateUtils.formatLocalDateTimeFromTimestampBySystemTimezone(Long.parseLong(soulContext.getTimestamp()));
final LocalDateTime now = LocalDateTime.now();
final long between = DateUtils.acquireMinutesBetween(start, now);
//跟delay做比对,timestap不能比现在早过5分钟
if (between > delay) {
return Pair.of(Boolean.FALSE, String.format(SoulResultEnum.SING_TIME_IS_TIMEOUT.getMsg(), delay));
}
return sign(soulContext, exchange);
}
/**
* verify sign .
*
* @param soulContext {@linkplain SoulContext}
* @return result : True is pass, False is not pass.
*/
private Pair<Boolean, String> sign(final SoulContext soulContext, final ServerWebExchange exchange) {
final AppAuthData appAuthData = SignAuthDataCache.getInstance().obtainAuthData(soulContext.getAppKey());
//appKey是"认证管理"页面分发出去的,否者验签失败
if (Objects.isNull(appAuthData) || !appAuthData.getEnabled()) {
log.error("sign APP_kEY does not exist or has been disabled,{}", soulContext.getAppKey());
return Pair.of(Boolean.FALSE, Constants.SIGN_APP_KEY_IS_NOT_EXIST);
}
List<AuthPathData> pathDataList = appAuthData.getPathDataList();
if (CollectionUtils.isEmpty(pathDataList)) {
log.error("You have not configured the sign path:{}", soulContext.getAppKey());
return Pair.of(Boolean.FALSE, Constants.SIGN_PATH_NOT_EXIST);
}
//查看path是否在配置列表中
boolean match = pathDataList.stream().filter(AuthPathData::getEnabled)
.anyMatch(e -> PathMatchUtils.match(e.getPath(), soulContext.getPath()));
if (!match) {
log.error("You have not configured the sign path:{},{}", soulContext.getAppKey(), soulContext.getRealUrl());
return Pair.of(Boolean.FALSE, Constants.SIGN_PATH_NOT_EXIST);
}
// 根据之前介绍的sign生成逻辑再生成一遍签名sign
String sigKey = SignUtils.generateSign(appAuthData.getAppSecret(), buildParamsMap(soulContext));
boolean result = Objects.equals(sigKey, soulContext.getSign());
//校验sign是否合法
if (!result) {
/ 不同验签失败
log.error("the SignUtils generated signature value is:{},the accepted value is:{}", sigKey, soulContext.getSign());
return Pair.of(Boolean.FALSE, Constants.SIGN_VALUE_IS_ERROR);
} else {
List<AuthParamData> paramDataList = appAuthData.getParamDataList();
if (CollectionUtils.isEmpty(paramDataList)) {
return Pair.of(Boolean.TRUE, "");
}
paramDataList.stream().filter(p ->
("/" + p.getAppName()).equals(soulContext.getContextPath()))
.map(AuthParamData::getAppParam)
.filter(StringUtils::isNoneBlank).findFirst()
.ifPresent(param -> exchange.getRequest().mutate().headers(httpHeaders -> httpHeaders.set(Constants.APP_PARAM, param)).build()
);
}
return Pair.of(Boolean.TRUE, "");
}
private Map<String, String> buildParamsMap(final SoulContext dto) {
Map<String, String> map = Maps.newHashMapWithExpectedSize(3);
map.put(Constants.TIMESTAMP, dto.getTimestamp());
map.put(Constants.PATH, dto.getPath());
map.put(Constants.VERSION, "1.0.0");
return map;
}
}
- 整体逻辑比较简单
- 用时间看请求是否在效期内5分钟内
- 用约定的签名方式同样生成出1个签名,如果和请求带的签名相同则表明验签通过。这套逻辑在sk\ak不丢失的情况下保证了请求这是合法有效的。
四、waf插件
4.1 配置
- 在网关的 pom.xml 文件中添加 waf 的支持。
<!-- soul waf plugin starter-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-plugin-waf</artifactId>
<version>${project.version}</version>
</dependency>
<!-- soul waf plugin end-->
- 在 soul-admin –> 插件管理-> waf 设置为开启。
- 插件编辑里面新增配置模式。
{"model":"black"}
- 默认为黑名单模式,设置值为 mixed 则为混合模式.
- 当 model 设置为 black 模式的时候,只有匹配的流量才会执行拒绝策略,不匹配的,直接会跳过。
- 当 model 设置为 mixed 模式的时候,所有的流量都会通过 waf插件,针对不同的匹配流量,用户可以设置是拒绝,还是通过。
- 配置selectorList
- 配置ruleList
- postman测试
可以看到waf插件已生效
4.2 源码解读
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
WafConfig wafConfig = Singleton.INST.get(WafConfig.class);
// 选择器和rule都为空时处理逻辑
if (Objects.isNull(selector) && Objects.isNull(rule)) {
// 如果是黑名单模式,则直接到下一个插件
if (WafModelEnum.BLACK.getName().equals(wafConfig.getModel())) {
return chain.execute(exchange);
}
// 如果是混合模式,则流量都会通过waf插件,默认都拒绝 403
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
Object error = SoulResultWrap.error(403, Constants.REJECT_MSG, null);
return WebFluxResultUtils.result(exchange, error);
}
String handle = rule.getHandle();
WafHandle wafHandle = GsonUtils.getInstance().fromJson(handle, WafHandle.class);
// 配置规则不完整,容错处理
if (Objects.isNull(wafHandle) || StringUtils.isBlank(wafHandle.getPermission())) {
log.error("waf handler can not configuration:{}", handle);
return chain.execute(exchange);
}
// 如果是拒绝策略,则获取设置的拒绝状态码,提示message直接返回
if (WafEnum.REJECT.getName().equals(wafHandle.getPermission())) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
Object error = SoulResultWrap.error(Integer.parseInt(wafHandle.getStatusCode()), Constants.REJECT_MSG, null);
return WebFluxResultUtils.result(exchange, error);
}
return chain.execute(exchange);
}
五、小结
-
sign插件
- pom引入sign插件
- 在soul-admin中开启sign-plugin插件
- admin端-System Manage-Authentication里添加一个AK/SK 记录
- sign插件里配置selector和rule
- 按规则填充headers参数,请求网关
-
waf插件
- pom引入waf插件
- 在soul-admin中开启waf-plugin插件,配置防火墙模式
- 配置selector和rule
- 请求验证