小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
接着上一篇 OpenFegin 实现接口参数签名认证(一),这篇我们将举例用 OpenFeign 来实现一个比较常见的接口参数签名认证,保证简简单单。
OpenFeign 是啥
Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-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 这个词本身就很直白的表达了他的核心作用,那就是套娃。
-
通过实现 OpenFeign 的 Client 接口,我们可以轻松的接入其他三方的HTTP客户端,比如我最爱的 okhttp 。官方直接支持的还有我们熟知的 HttpClient,GoogleHttpClient 等等。
这里为啥使用OpenFeign呢?那当然是微服务要用,引都引入了,不用那就太沙雕了 😆。通过OpenFeign,我们可以轻松的通过Spring Web的注解完成一个个的请求模板,无需学习新的东西,即可领会其各个注解的含义,就算先前没有使用过的新手也可以照葫芦画瓢,快速的上手。OpenFeign 功能作用很多,这里我就不多逼逼了,不怎么了解的小伙伴可以自行查看文档研究。今天这里我们主要讲解 OpenFeign 作为 RESTFUL 客户端来实现接口的参数签名校验。
假定一个三方SDK
为了实现一个比较常见且包含绝大部分业务场景的模板代码,现在我们假定有一个三方sdk,我们已经拿到了他的 请求地址 、appkey 和 appsecret 。
它的认证授权规则是这样的
- Query参数必须携带 appkey、timestamp 、nonce、sign
- Query参数的顺序必须是 appkey、timestamp 、nonce、sign
- Query参数必须在所有Query参数最后
- sign 的生成规则:
md5(key={appkey}&secret=md5({appsecret})&service={请求path}¶ms=md5({query参数+json字符串})×tamp={timestamp}&nonce={nonce})
- sign 中 请求path 为请求地址中非 baseUrl 、非 query参数 的部分
它有两个api接口
- 保存用户
POST /xxx-api/xxx/save-user
# 请求体
{
"name": "林大俊",
"sex": 2,
"age": 18
}
# 响应体
{
"code": 0,
"msg": "success",
"data": 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
,先写好基础代码,把已知的内容 baseUrl
、 appkey
、 appsecret
给放进去,为正式开整做好准备
@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
里面有些啥,方便我们后面行事
- 首先,这是我们众多三方sdk中的一个,那么这个拦截器,就应该只处理这个sdk的请求,我们应该先排除非当前sdk地址的请求。咱们先找找上面的图,看看怎么获取当前请求的地址。我们可以看到
RequestTemplate
里面有个url()
方法,但是很抱歉,这个不行的。这个url()
只包含我们在client
中声明的 url 和 path 部分,并没有包含 baseUrl。所以我们还得找找其他方法,仔细查找一番之后,发现并没有发现可以直接获取到带 baseUrl 的api???Feign应该不会这么傻,让我们获取不到 baseUrl 吧?我们找找看看有没有什么返回封装对象的东东,经过一番点击,我们可以发现一个Target
这样的东东,看下源码
确认过眼神,这就是我要找的东东。在 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¶ms=%s×tamp=%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¶ms=%s×tamp=%s&nonce=%s",
appKey,
DigestUtils.md5Hex(appSecret),
service,
DigestUtils.md5Hex(params),
timeStamp,
nonce
);
return DigestUtils.md5Hex(sign);
}
}
总结
-
这里我们仅仅实现了
参数
和json请求体
的实现,实际的应用场景中可能还有表单,文件上传等等。这时候我们仅仅需要对这不同的请求方式、请求内容做不同的操作,这些文档一般都会给清楚,我们只需要根据文档做相应的调整即可。 -
OpenFeign 的
RequestTemplate
还是比较强大的,基本常见的功能都能够满足,用起来非常的nice,如果有些更奇怪的需求,也可以通过自定义实现完成。各种api也比较贴合正常的使用体验,基本不需要看什么文档,看名字即可顺滑编码。 -
不要觉得接口参数签名这个东西比较难,只要找对了思路,编码仅仅只是很小的一部分,其他语言也可以类比一下,操作大同小异。思路对了就ok了
因为时间关系,写的比较仓促,文章到这里就结束了。咋们下次继续😁
如果文章对您有帮助的话,欢迎 点赞
、 评论
、 关注
、 收藏
、 分享
,您的支持是我码字的动力,万分感谢!!!🌈
如果文章内容出现错误的地方,欢迎指正,交流,谢谢😘