在上一节中,我们尝试dubbo插件将http协议转换成dubbo协议,今天我们来看看 sign 插件是如何实现请求鉴权的。
流程演示
我们先试试没有添加sign插件时,是否能正常请求接口:
➜ ~ curl localhost:9195/dubbo/findAll
{"code":200,"message":"Access to success!","data":{"name":"hello world Soul Alibaba Dubbo , findAll","id":"-803129909"}}
成功。
首先我们需要在插件管理中打开sign插件的开关。然后我们在认证管理中,新增一条 AK/SK 记录:
添加完毕后,我们可以看到AppKey 和加密密钥已经为我们生成好了:
现在我们再试试刚刚代理dubbo 服务中的 /dubbo/findAll 接口:
➜ ~ curl localhost:9195/dubbo/findAll
{"code":200,"message":"Access to success!","data":{"name":"hello world Soul Alibaba Dubbo , findAll","id":"-541002115"}}
发现没有生效,原因是没有配置sign的选择器:
按照上图所示配置好sign选择器后再请求我们配置了鉴权校验的接口:
➜ ~ curl localhost:9195/dubbo/findAll
{"code":401,"message":"sign parameters are incomplete!","data":null}
成了,提示我们签名参数不完整,下表展示了我们需要在headers中添加的header:
字段 | 值 | 描述 |
---|---|---|
timestamp | 1571711067186 | 上述你进行签名的时候使用的时间值 |
appKey | 1TEST123456781 | AK值 |
sign | A90E66763793BDBC817CF3B52AAAC041 | 上述得到的签名值 |
version | 1.0.0 | 固定默认值 |
签名插件会默认过滤5分钟之后的请求。
现在直奔单元测试类,把现有的时间戳,appkey 和 sign 改写成自己的:
private SignService signService;
private ServerWebExchange exchange;
//这里改成自己的
private final String appKey = "E841FEDBF8EE4912878993EC84F70947";
//这里改成自己的
private final String secretKey = "4FBB99E1B97B45D79D8F891D45DBB97E";
private SoulContext passed;
@Value("${soul.sign.delay:5}")
private int delay;
@Before
public void setup() {
this.signService = new DefaultSignService();
this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localhost").build());
//这里改成自己的,路径是默认签名的一部分
final String path = "/dubbo/findAll";
PluginData signData = new PluginData();
signData.setId("1");
signData.setName(PluginEnum.SIGN.getName());
signData.setEnabled(true);
signData.setRole(1);
BaseDataCache.getInstance().cachePluginData(signData);
AppAuthData authData = new AppAuthData();
authData.setAppKey(appKey);
authData.setAppSecret(secretKey);
authData.setEnabled(true);
AuthPathData authPathData = new AuthPathData();
authPathData.setAppName("test-api");
authPathData.setPath(path);
authPathData.setEnabled(true);
authData.setPathDataList(Lists.newArrayList(authPathData));
SignAuthDataCache.getInstance().cacheAuthData(authData);
this.passed = new SoulContext();
this.passed.setModule("/dubbo");
this.passed.setMethod("/findAll");
this.passed.setRpcType("dubbo");
this.passed.setHttpMethod("GET");
this.passed.setPath(path);
final String timestamp = String.valueOf(System.currentTimeMillis());
this.passed.setTimestamp(timestamp);
this.passed.setSign(buildSign(secretKey, timestamp, this.passed.getPath()));
//在这里打断点,获取到 this.getpassed 中的sign
this.passed.setAppKey(appKey);
this.passed.setContextPath("/");
this.passed.setRealUrl("/dubbo/findAll");
}
下面我们在postman 请求的 headers 中添加签名相关的header并请求:
成功通过了校验。
源码分析
/**
* 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做比对
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());
//一系列判空逻辑
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);
}
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;
}
}
总结
其实签名插件的核心逻辑比较简单,就是实现了 sign 、 verify等方法,再实现 SignService 接口的 signVerify函数完成整个签名的流程。如果有需要我们可以加入自己的签名校验逻辑,比如用JWT之类的来实现,只需要实现 SignService 接口的 signVerify 即可。