【JAVA】OpenFegin 实现接口参数签名认证(二)

2,353 阅读7分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

接着上一篇 OpenFegin 实现接口参数签名认证(一),这篇我们将举例用 OpenFeign 来实现一个比较常见的接口参数签名认证,保证简简单单。

OpenFeign 是啥

Feign is a Java to HTTP client binder inspired by RetrofitJAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.

通俗的话讲:

  • OpenFeign 就是一种轻量的声明式Http客户端,通过对注解的解析,以实现模板化的请求。

  • OpenFeign 可以轻易的替换各种已有的请求库作为底层HTTP库。Feign 这个词本身就很直白的表达了他的核心作用,那就是套娃。

  • 通过实现 OpenFeignClient 接口,我们可以轻松的接入其他三方的HTTP客户端,比如我最爱的 okhttp 。官方直接支持的还有我们熟知的 HttpClientGoogleHttpClient 等等。

  这里为啥使用OpenFeign呢?那当然是微服务要用,引都引入了,不用那就太沙雕了 😆。通过OpenFeign,我们可以轻松的通过Spring Web的注解完成一个个的请求模板,无需学习新的东西,即可领会其各个注解的含义,就算先前没有使用过的新手也可以照葫芦画瓢,快速的上手。OpenFeign 功能作用很多,这里我就不多逼逼了,不怎么了解的小伙伴可以自行查看文档研究。今天这里我们主要讲解 OpenFeign 作为 RESTFUL 客户端来实现接口的参数签名校验。

假定一个三方SDK

  为了实现一个比较常见且包含绝大部分业务场景的模板代码,现在我们假定有一个三方sdk,我们已经拿到了他的 请求地址appkey 和 appsecret

它的认证授权规则是这样的

  • Query参数必须携带 appkeytimestampnoncesign
  • Query参数的顺序必须是 appkeytimestampnoncesign
  • Query参数必须在所有Query参数最后
  • sign 的生成规则: md5(key={appkey}&secret=md5({appsecret})&service={请求path}&params=md5({query参数+json字符串})&timestamp={timestamp}&nonce={nonce})
  • sign请求path 为请求地址中非 baseUrl 、非 query参数 的部分

它有两个api接口

  1. 保存用户
POST /xxx-api/xxx/save-user

# 请求体
{
  "name": "林大俊",
  "sex": 2,
  "age": 18
}

# 响应体
{
  "code": 0,
  "msg": "success",
  "data": 1
}

  1. 查询用户列表
GET /xxx-api/xxx/user-list

# 请求体
?id=123456&curr=1&size=10

# 响应体
{
  "code": 0,
  "msg": "success",
  "data": {
    "size": 10,
    "curr": 1,
    "total": 1,
    "list": [
      {
        "uid":1,
        "name": "林大俊",
        "sex": 2,
        "age": 18
      }
    ]
  }
}

代码实现

XXXSdk的认证、接口有了,现在我们先声明一下 FeignClient

@FeignClient(value = "UserService", url = "${xxxsdk.baseUrl}", path = "/xxx-api/xxx")
public interface XXXSDKUserService {

    /**
     * 保存用户
     */
    @PostMapping("/save-user")
    R<Long> saveUser(@RequestBody SaveUserReq req);

    /**
     * 查询用户列表
     */
    @GetMapping("/user-list")
    R<Paged<UserListPagedRep>> userList(@SpringQueryMap UserListPagedReq req);
}

这里请求体和响应体就不贴了,大家应该也能猜到有些啥。眼尖的同学可能发现我用了一个和 Spring Web 无关的注解 @SpringQueryMap ,并且 GET 请求也没有使用 @RequestParam,而是用的一个参数对象,这里我也不多讲,有机会出一篇文章来详细讲讲 OpenFeign ,有兴趣的同学可以自行先了解一下 ,这个注解它在 spring-cloud-openfeign-core 里面,懂的直接略过吧。

现在我们 Client 有了,参数签名以及附带认证参数在哪儿添加呢?这个还需要考虑?答案肯定是在拦截器里面做咯

别的不多说,直接实现 RequestInterceptor ,先写好基础代码,把已知的内容 baseUrlappkeyappsecret 给放进去,为正式开整做好准备

@Component
@Slf4j
public class XXXSDKAuthInterceptor implements RequestInterceptor {
    @Value("${xxxsdk.baseUrl}")
    private String baseUrl;
    @Value("${xxxsdk.appkey}")
    private String appKey;
    @Value("${xxxsdk.appsecret}")
    private String appSecret;
    @Override
    public void apply(RequestTemplate template) {
    }
}

基础代码有了,那就正式开干🤣,先贴一下 RequestTemplate 里面有些啥,方便我们后面行事

RequestTemplate.png

  • 首先,这是我们众多三方sdk中的一个,那么这个拦截器,就应该只处理这个sdk的请求,我们应该先排除非当前sdk地址的请求。咱们先找找上面的图,看看怎么获取当前请求的地址。我们可以看到 RequestTemplate 里面有个 url() 方法,但是很抱歉,这个不行的。这个 url() 只包含我们在 client 中声明的 urlpath 部分,并没有包含 baseUrl。所以我们还得找找其他方法,仔细查找一番之后,发现并没有发现可以直接获取到带 baseUrl 的api???Feign应该不会这么傻,让我们获取不到 baseUrl 吧?我们找找看看有没有什么返回封装对象的东东,经过一番点击,我们可以发现一个 Target 这样的东东,看下源码

image.png

确认过眼神,这就是我要找的东东。在 RequestTemplate 中,我们可以直接通过 feignTarget() 获取到 Target ,这个方法是被标记成 @Experimental 实验性质的,但是并不影响我们使用。现在有了 baseUrl,我们直接将不是当前 baseUrl 的请求,直接 return

String url = template.feignTarget().url();
log.info("url: <{}>, baseUrl: <{}>", url, baseUrl);
if (!url.startsWith(baseUrl)) return;
  • 第二步,我们需要根据请求的类型,获取对应的请求参数。现在先来处理 GET 请求。GET 请求的参数获取就比较明显,通过 queryLine() 这个方法我们就可以得到最终拼接好参数的query字符串,但是还需要处理一下开头的 ? 号,下面我们实现一下这部分逻辑
    String params = template.queryLine();
    if (StringUtils.isNotBlank(params)) {
        // 移除query字符串开头的问号: ?xxx=123 => xxx=123
        params = params.substring(1);
    }

接着是 POST 请求。POST 我们需要先拿到请求体,在 RequestTemplate 中也比较直观,我们可以一眼找到 body() 这个方法,但是这个 body() 返回的是一个 byte[] 数组,我们还需要将其转换成字符串,在转换之前,我们还需要知道这个 body() 的编码方式才行。requestCharset() 不就是为我们量身订制的吗?下面我们来完成这部分

    byte[] body = template.body();
    String json = new String(body, template.requestCharset());
  • 第三步,参数有了,我们还需要一个方法来生成这个 sign ,这个就没有多少可以说的咯,直接按照规则,字符串拼接一下即可
private String sign(
        final String service,
        final String params,
        final long timeStamp,
        final String nonce
) {
    String sign = String.format(
            "key=%s&secret=%s&service=%s&params=%s&timestamp=%s&nonce=%s",
            appKey,
            DigestUtils.md5Hex(appSecret),
            service,
            DigestUtils.md5Hex(params),
            timeStamp,
            nonce
    );
    return DigestUtils.md5Hex(sign);
}
  • 最后,再整个方法把参数给附带到请求参数后面去
private void applyQuerySign(final RequestTemplate template, final String params) {
    long timestamp = System.currentTimeMillis();
    String path = template.path();
    String nonce = UUID.randomUUID().toString().replaceAll("-", "");
    String sign = sign(path, params, timestamp, nonce);
    template.query("appkey", appKey)
            .query("timestamp", String.valueOf(timestamp))
            .query("nonce", nonce)
            .query("sign", sign);
}

大功告成,最后汇总下代码,最终实现如下

@Component
@Slf4j
public class XXXSDKAuthInterceptor implements RequestInterceptor {
    @Value("${xxxsdk.baseUrl}")
    private String baseUrl;
    @Value("${xxxsdk.appkey}")
    private String appKey;
    @Value("${xxxsdk.appsecret}")
    private String appSecret;

    @Override
    public void apply(RequestTemplate template) {
        // 获取请求基础地址
        String url = template.feignTarget().url();
        log.debug("url: {}, baseUrl: {}", url, baseUrl);
        // 如果请求的地址不是三方sdk,直接跳过
        if (!url.startsWith(baseUrl)) return;
        String method = template.method();
        log.debug("method: {}", method);
        String params = template.queryLine();
        boolean hasQuery = StringUtils.isNotBlank(params);
        if (hasQuery) {
            // 去除请求参数开头的 <?>
            params = params.substring(1);
        }
        byte[] body = template.body();
        if (!ObjectUtils.isEmpty(body)) {
            // 将请求体还原成json串
            String json = new String(body, template.requestCharset());
            if (hasQuery) {
                params += "&" + json;
            } else {
                // 如果没有参数,直接把json赋值给参数
                params = json;
            }
        }
        applyQuerySign(template, params);
    }

    private void applyQuerySign(final RequestTemplate template, final String params) {
        long timestamp = System.currentTimeMillis();
        String path = template.path();
        String nonce = UUID.randomUUID().toString().replaceAll("-", "");
        String sign = sign(path, params, timestamp, nonce);
        log.debug(
                "appkey: <{}>, timestamp: <{}>, nonce: <{}>, sign: <{}>",
                appKey, timestamp, nonce, sign);
        template.query("appkey", appKey)
                .query("timestamp", String.valueOf(timestamp))
                .query("nonce", nonce)
                .query("sign", sign);
    }

    private String sign(
            final String service,
            final String params,
            final long timeStamp,
            final String nonce
    ) {
        String sign = String.format(
                "key=%s&secret=%s&service=%s&params=%s&timestamp=%s&nonce=%s",
                appKey,
                DigestUtils.md5Hex(appSecret),
                service,
                DigestUtils.md5Hex(params),
                timeStamp,
                nonce
        );
        return DigestUtils.md5Hex(sign);
    }
}

总结

  • 这里我们仅仅实现了 参数json请求体 的实现,实际的应用场景中可能还有表单,文件上传等等。这时候我们仅仅需要对这不同的请求方式、请求内容做不同的操作,这些文档一般都会给清楚,我们只需要根据文档做相应的调整即可。

  • OpenFeignRequestTemplate 还是比较强大的,基本常见的功能都能够满足,用起来非常的nice,如果有些更奇怪的需求,也可以通过自定义实现完成。各种api也比较贴合正常的使用体验,基本不需要看什么文档,看名字即可顺滑编码。

  • 不要觉得接口参数签名这个东西比较难,只要找对了思路,编码仅仅只是很小的一部分,其他语言也可以类比一下,操作大同小异。思路对了就ok了

因为时间关系,写的比较仓促,文章到这里就结束了。咋们下次继续😁

src=http___c-ssl.duitang.com_uploads_item_201910_06_20191006165431_xfeny.thumb.1000_0.gif&refer=http___c-ssl.duitang.gif


如果文章对您有帮助的话,欢迎 点赞 、 评论 、 关注 、 收藏 、 分享 ,您的支持是我码字的动力,万分感谢!!!🌈

如果文章内容出现错误的地方,欢迎指正,交流,谢谢😘

参考链接