MAC/HMAC的实现与外部程序调用接口及其权限配置
当一个外部程序要调用我当前程序的接口时,不可避免的会出现权限问题,现在就来看对于外部用户如何调用我们程序中的内部接口。

但是对于一个第三方的程序,如果我们给他一个账号登录让他获取token,虽然也是一个办法,但是在场景上可能适用性并非最符合的。下面我会先介绍一下HMAC是什么,然后比较HMAC和账号密码获取token的优劣,最后会放出JAVA若依框架下的实现。
一. HMAC
- 双方共享一个密钥 K;
- 发送方计算 T = HMAC(K, 消息),把 T 贴在消息后面;
- 接收方用同样的 K 再算一遍,比对 T;
- 作用:防篡改、防重放(配合序号/时间戳),但不能防否认——因为双方都知道 K,谁都可以伪造。
典型标准:RFC 2104(HMAC)、AES-CMAC、GMAC(GCM 模式内置)。
二. HMAC与账号密码获取token比较
- 账号密码方案:
- 客户端 → 服务器:
username=bob&password=123456
一旦被中间人抓到(日志、代理、抓包、DNS 劫持)就等于永久泄露身份。 - 账号密码要先调
/login→ 返回JWT/Session→ 再调业务接口,至少两次 RTT;
- 客户端 → 服务器:
- HMAC 方案:
- 客户端 → 服务器:每次请求把 时间戳、随机数、方法、URI、请求体 都算进签名;签名只能使用一次,重放无效,密钥本身从未出现在报文里。
- MAC 只要客户端本地计算一次摘要,单请求即可完成鉴权,服务器端只做“计算&比对签名”。
三. Java若依框架下的HMAC实现
本次使用的HMAC,就是用 T=HMAC(时间戳,随机数,K)方式来获得相应的签名。实际在接口验证访问的过程中,还可以加入ip地址白名单进行验证。
首先,编写了一个接口用以外部调用,
model是传入的参数,在这边不需要。接口相对路径是/externalCall/callHomePageInfo1
2
3
4
5
6
7
8
9
public class ExternalCallController {
public R<String> callHomePageInfo( List<ExtraScoreSequenceDTO> model) {
return R.ok("success");
}
}对于上述接口,若依框架默认是走账号密码的
Token认证,因此需要先把该接口的Token认证给关掉。在framework.config.SecurityConfig中的configure方法中的httpSecurity加上需要关掉Token认证的接口路径1
2
3httpSecurity.
...
.antMatchers(接口路径1,接口路径2,"/externalCall/callHomePageInfo")...新建一个 IP+签名拦截器,只拦
/externalCall/callHomePageInfo。这边有白名单,如果不想要的话可以注释掉。这个得自己加一下,在framework.interceptor新建一个类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ThirdApiInterceptor implements HandlerInterceptor {
/* ---------- 1. IP 白名单 ---------- */
private static final Set<String> ALLOW_IP;
static {
Set<String> tmp = new HashSet<>();
tmp.add("127.0.0.1"); // 第三方出口 IP
ALLOW_IP = Collections.unmodifiableSet(tmp);
}
/* ---------- 2. 与第三方约定的密钥 ---------- */
private static final String API_SECRET = "F4A9E9C5F3B0D1A4F5E8E7D6C4B3A2D1";
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
/* 1. 校验 IP */
String ip = IpUtils.getIpAddr(request);
if (!ALLOW_IP.contains(ip)) {
response.setStatus(403);
response.getWriter().write("IP not allowed");
return false;
}
/* 2. 取头部 */
String ts = request.getHeader("X-Ts");
String nonce = request.getHeader("X-Nonce");
String sign = request.getHeader("X-Sign");
/* 3. 判空 + 时效(5 min) */
long now = System.currentTimeMillis() / 1000;
if (ts == null || nonce == null || sign == null || Math.abs(now - Long.parseLong(ts)) > 300) {
response.setStatus(403);
response.getWriter().write("sign error/timeout");
return false;
}
/* 4. 验签 md5(ts + nonce + secret) */
String build = DigestUtils.md5Hex((ts + nonce + API_SECRET)
.getBytes(StandardCharsets.UTF_8));
if (!build.equalsIgnoreCase(sign)) {
response.setStatus(403);
response.getWriter().write("sign error");
return false;
}
return true;
}
}把拦截器注册到 MVC 配置(只针对
/externalCall/callHomePageInfo)。在framework.config下新建WebMvcConfig调用前面写好的拦截器。1
2
3
4
5
6
7
8
9
10
11
12
class WebMvcConfig implements WebMvcConfigurer {
private ThirdApiInterceptor thirdApiInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
// 原有拦截器不动
registry.addInterceptor(thirdApiInterceptor)
.addPathPatterns("/externalCall/callHomePageInfo");
}
}写一个测试程序,调用该接口或者用单独的
Webhook进行调用,验证是否获得需要的数据。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class ExternalCallController {
private static final String API_SECRET = "F4A9E9C5F3B0D1A4F5E8E7D6C4B3A2D1";
public R<String> test() {
// 时间戳
String ts = String.valueOf(System.currentTimeMillis()/1000);
// 随机数
String nonce= UUID.randomUUID().toString();
// 签名,可以换成任意MD5计算的包
String sign = DigestUtils.md5Hex(ts + nonce + API_SECRET);
List<ExtraScoreSequenceDTO> model = new ArrayList<>();
HttpResponse resp = HttpRequest.post("http://你的ip地址:你的端口/externalCall/callHomePageInfo")
.header("X-Ts", ts)
.header("X-Nonce", nonce)
.header("X-Sign", sign)
.body(JSONUtil.toJsonStr(model))
.execute();
// 转为可以使用的格式
JSONObject entries = JSONUtil.parseObj(resp.body());
JSONObject data = entries.getJSONObject("data");
if(data == null){
throw new BaseException(entries.getStr("msg"));
}
HomePageAllInfoResult bean = data.toBean(HomePageAllInfoResult.class);
return R.ok(bean);
}
}
四. At Last
一句话:
“用 HMAC 不是为了代替‘账号+密码’,而是为了让第三方在 不传输任何秘密 的前提下,就能向服务器证明‘我确实拥有密钥’,并且‘这条消息中途没被篡改’。”
这能够更好的帮助我们适应第三方程序调用的场景。
结束,拜拜!

- 标题: MAC/HMAC的实现与外部程序调用接口及其权限配置
- 作者: Sabthever
- 创建于 : 2025-10-11 10:38:13
- 更新于 : 2025-10-21 16:20:24
- 链接: https://sabthever.cn/2025/10/11/technology/security/外部程序调用接口/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。