MAC/HMAC的实现与外部程序调用接口及其权限配置

Sabthever

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

↑假的
  首先我们要知道,对于接口而言我们往往是通过登录后获得相应的`token`,然后浏览器会与后端接口进行交互进行`JwtAuthenticationToken`的认证后,后端进行相应操作。

  但是对于一个第三方的程序,如果我们给他一个账号登录让他获取token,虽然也是一个办法,但是在场景上可能适用性并非最符合的。下面我会先介绍一下HMAC是什么,然后比较HMAC和账号密码获取token的优劣,最后会放出JAVA若依框架下的实现。

一. HMAC

  • 双方共享一个密钥 K;
  • 发送方计算 T = HMAC(K, 消息),把 T 贴在消息后面;
  • 接收方用同样的 K 再算一遍,比对 T;
  • 作用:防篡改、防重放(配合序号/时间戳),但不能防否认——因为双方都知道 K,谁都可以伪造。

典型标准:RFC 2104(HMAC)、AES-CMAC、GMAC(GCM 模式内置)。

二. HMAC与账号密码获取token比较

  • 账号密码方案:
    1. 客户端 → 服务器:username=bob&password=123456
      一旦被中间人抓到(日志、代理、抓包、DNS 劫持)就等于永久泄露身份。
    2. 账号密码要先调 /login → 返回 JWT/Session → 再调业务接口,至少两次 RTT
  • HMAC 方案:
    1. 客户端 → 服务器:每次请求把 时间戳、随机数、方法、URI、请求体 都算进签名;签名只能使用一次,重放无效密钥本身从未出现在报文里
    2. MAC 只要客户端本地计算一次摘要,单请求即可完成鉴权,服务器端只做“计算&比对签名”。

三. Java若依框架下的HMAC实现

  本次使用的HMAC,就是用 T=HMAC(时间戳,随机数,K)方式来获得相应的签名。实际在接口验证访问的过程中,还可以加入ip地址白名单进行验证。

  1. 首先,编写了一个接口用以外部调用,model是传入的参数,在这边不需要。接口相对路径是/externalCall/callHomePageInfo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController@RequestMapping("/externalCall")
    @Api(tags = "外部调用")
    public class ExternalCallController {
    @ApiOperation("首页接口调用")
    @PostMapping("/callHomePageInfo")
    public R<String> callHomePageInfo(@RequestBody List<ExtraScoreSequenceDTO> model) {
    return R.ok("success");
    }
    }
  2. 对于上述接口,若依框架默认是走账号密码的Token认证,因此需要先把该接口的Token认证给关掉。在framework.config.SecurityConfig中的configure方法中的httpSecurity加上需要关掉Token认证的接口路径

    1
    2
    3
    httpSecurity.
    ...
    .antMatchers(接口路径1,接口路径2,"/externalCall/callHomePageInfo")...
  3. 新建一个 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
    @Componentpublic 
    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";
    @Override
    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;
    }
    }
  4. 把拦截器注册到 MVC 配置(只针对 /externalCall/callHomePageInfo)。在framework.config下新建WebMvcConfig调用前面写好的拦截器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Configurationpublic 
    class WebMvcConfig implements WebMvcConfigurer {
    @Resource
    private ThirdApiInterceptor thirdApiInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    // 原有拦截器不动
    registry.addInterceptor(thirdApiInterceptor)
    .addPathPatterns("/externalCall/callHomePageInfo");
    }
    }
  5. 写一个测试程序,调用该接口或者用单独的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
    28
    public class ExternalCallController {
    private static final String API_SECRET = "F4A9E9C5F3B0D1A4F5E8E7D6C4B3A2D1";
    @ApiOperation("测试")
    @PostMapping("/test")
    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 进行许可。
目录
MAC/HMAC的实现与外部程序调用接口及其权限配置