轻松实现开放接口的签名和验签

原创 一安 一安未来 2023-12-01 08:00 发表于北京

收录于合集#干货分享集191个

图片

大家好,我是一安~

介绍

开放接口

开放接口是指不需要登录凭证就允许被第三方系统调用的接口,这个时候肯定要考虑接口数据的安全性问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题,为了防止开放接口被恶意调用,开放接口一般都需要验签才能被调用。

验签

验签是指第三方系统在调用接口之前,需要按照接口提供方的规则根据所有请求参数生成一个签名(字符串),在调用接口时携带该签名。接口提供方会验证签名的有效性,只有签名验证有效才能正常调用接口,否则请求会被驳回。

大致流程

图片

实战

这里只是演示,未真正区分开客户端和服务端

导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.7.13</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.15</version>
    </dependency>

自定义配置

私钥和公钥直接保存在文件中

spring:
  redis:
    host: localhost
    port: 6379
    password: root
    database: 2

signature:
  enable: true
  key-pair:
    # 调用方ID
    test-1:
      # 算法
      algorithm: SHA256withRSA
      # 私钥
      private-key-path: classpath:signature/private
      # 公钥
      public-key-path: classpath:signature/public
      # 生效时间(分钟)
      effective-time: 3

加载配置信息

@Data
@ConditionalOnProperty(value = "signature.enable", havingValue = "true")
@Component
@ConfigurationProperties("signature")
public class SignatureProps {

    private Boolean enable;
    private Map<String, KeyPairProps> keyPair;

    @Data
    public static class KeyPairProps {
        private SignAlgorithm algorithm;
        private String publicKeyPath;
        private String publicKey;
        private String privateKeyPath;
        private String privateKey;
        private Integer effectiveTime;
    }
}

签名管理类

@ConditionalOnBean(SignatureProps.class)
@Component
public class SignatureManager {

    private final SignatureProps signatureProps;
    public SignatureManager(SignatureProps signatureProps) {
        this.signatureProps = signatureProps;
        loadKeyPairByPath();
    }
    
    public SignatureProps.KeyPairProps getKeyPairPropsByClientID(String clientID) {
        return signatureProps.getKeyPair().get(clientID);
    }   
    

    /**
     * 验签。验证不通过可能抛出运行时异常CryptoException
     *
     * @param clientID  调用方的唯一标识
     * @param rawData   原数据
     * @param signature 待验证的签名(十六进制字符串)
     * @return 验证是否通过
     */
    public boolean verifySignature(String clientID, String rawData, String signature) {
        Sign sign = getSignByClientID(clientID);
        if (ObjectUtils.isEmpty(sign)) {
            return false;
        }
        // 使用公钥验签
        return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(signature));
    }

    /**
     * 生成签名
     *
     * @param clientID 调用方的唯一标识
     * @param rawData  原数据
     * @return 签名(十六进制字符串)
     */
    public String sign(String clientID, String rawData) {
        Sign sign = getSignByClientID(clientID);
        if (ObjectUtils.isEmpty(sign)) {
            return null;
        }
        return sign.signHex(rawData);
    }

    private Sign getSignByClientID(String clientID) {
        SignatureProps.KeyPairProps keyPairProps = signatureProps.getKeyPair().get(clientID);
        if (ObjectUtils.isEmpty(keyPairProps)) {
            return null; // 无效的、不受信任的调用方
        }
        return SecureUtil.sign(keyPairProps.getAlgorithm(), keyPairProps.getPrivateKey(), keyPairProps.getPublicKey());
    }

    /**
     * 加载非对称密钥对
     */
    private void loadKeyPairByPath() {
        // 支持类路径配置,形如:classpath:signature/public
        // 公钥和私钥都是base64编码后的字符串
        signatureProps.getKeyPair()
                .forEach((key, keyPairProps) -> {
                    keyPairProps.setPublicKey(loadKeyByPath(keyPairProps.getPublicKeyPath()));
                    keyPairProps.setPrivateKey(loadKeyByPath(keyPairProps.getPrivateKeyPath()));
                    if (ObjectUtils.isEmpty(keyPairProps.getPublicKey()) ||
                            ObjectUtils.isEmpty(keyPairProps.getPrivateKey())) {
                        throw new RuntimeException("No public and private key files configured");
                    }
                });
    }

    private String loadKeyByPath(String path) {
        if (ObjectUtils.isEmpty(path)) {
            return null;
        }
        return IoUtil.readUtf8(ResourceUtil.getStream(path));
    }
}

自定义验签注解

自定义验签注解,控制哪些接口需要验签

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface VerifySignature {
    boolean resubmit() default true;//允许重复请求
}

切面验签

@ConditionalOnBean(SignatureProps.class)
@Component
@Slf4j
@Aspect
public class RequestSignatureAspect {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private SignatureManager signatureManager;

    @Pointcut("@annotation(org.example.sign.anno.VerifySignature)")
    public void signPointCut() {
    }

    @Before("signPointCut()")
    public void verifySignature(JoinPoint joinPoint) {
    
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        String clazz = joinPoint.getTarget().getClass().getName();
        String name = method.getName();

        VerifySignature verifySignature = method.getAnnotation(VerifySignature.class);
        if(!verifySignature.resubmit()&&!redisTemplate.opsForValue().setIfAbsent(clazz+":"+name,"1",10, TimeUnit.SECONDS)){
            throw new RuntimeException("不可重复提交");
        } 
    
        HttpServletRequest request = getHttpServletRequest();
        // 从请求头中提取调用法ID,不存在直接驳回
        String clientID = request.getHeader("clientID");
        if (ObjectUtils.isEmpty(clientID)) {
            throw new RuntimeException("不受信任的调用方");
        }

        // 从请求头中提取签名,不存在直接驳回
        String signature = request.getHeader("sign");
        if (ObjectUtils.isEmpty(signature)) {
            throw new RuntimeException("无效的签名");
        }
        
         // 从请求头中提取时间戳,不存在或者过期直接驳回
        String timestamp = request.getHeader("timestamp");
        if (ObjectUtils.isEmpty(timestamp) ||
                ChronoUnit.MINUTES.between(
                        LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(timestamp)), ZoneOffset.ofHours(8)),
                        LocalDateTime.now()
                ) > signatureManager.getKeyPairPropsByClientID(clientID).getEffectiveTime()) {
            throw new RuntimeException("签名已过期");
        }


        // 提取请求参数
        String requestParamsStr = extractRequestParams(request);

        // 验签。验签不通过抛出业务异常
        verifySignature(clientID, requestParamsStr, signature);
    }

    /**
     * 由于body输入流只能读取一次,所以拦截器中不能直接读取body的输入流,否则会造成后续@RequestBody的参数解析器读取不到body,因此需要使用ContentCachingRequestWrapper包装请求,缓存body内容
     **/
    private String extractRequestParams(HttpServletRequest request) {
        // @RequestBody
        String body = null;
        if (request instanceof ContentCachingRequestWrapper) {
            ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
            body = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
        }

        // @RequestParam
        Map<String, String[]> paramMap = request.getParameterMap();

        // @PathVariable
        ServletWebRequest webRequest = new ServletWebRequest(request, null);
        Map<String, String> uriTemplateVarNap = (Map<String, String>) webRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);

        return CommonUtils.extractRequestParams(body, paramMap, uriTemplateVarNap);
    }


    /**
     * 验证请求参数的签名
     */
    public void verifySignature(String clientID, String requestParamsStr, String signature) {
        try {
            boolean verified = signatureManager.verifySignature(clientID, requestParamsStr, signature);
            if (!verified) {
                throw new RuntimeException("The signature verification result is false.");
            }
        } catch (Exception ex) {
            log.error("Failed to verify signature", ex);
            throw new RuntimeException("业务异常");
        }
    }

    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        return sra.getRequest();
    }
}

工具类

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}
public class CommonUtils {
    /**
     * 提取所有的请求参数,按照固定规则拼接成一个字符串
     *
     * @param body              post请求的请求体
     * @param paramMap          路径参数(QueryString)。形如:name=zhangsan&age=18&label=A&label=B
     * @param uriTemplateVarNap 路径变量(PathVariable)。形如:/{name}/{age}
     * @return 所有的请求参数按照固定规则拼接成的一个字符串
     */
    public static String extractRequestParams(String body, Map<String, String[]> paramMap, Map<String, String> uriTemplateVarNap) {
        String paramStr = null;
        if (!ObjectUtils.isEmpty(paramMap)) {
            paramStr = paramMap.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(entry -> {
                        // 拷贝一份按字典序升序排序
                        String[] sortedValue = Arrays.stream(entry.getValue()).sorted().toArray(String[]::new);
                        return entry.getKey() + "=" + joinStr(",", sortedValue);
                    })
                    .collect(Collectors.joining("&"));
        }

        String uriVarStr = null;
        if (!ObjectUtils.isEmpty(uriTemplateVarNap)) {
            uriVarStr = joinStr(",", uriTemplateVarNap.values().stream().sorted().toArray(String[]::new));
        }

        // { userID: "xxx" }#name=zhangsan&age=18&label=A,B#zhangsan,18
        return joinStr("#", body, paramStr, uriVarStr);
    }

    /**
     * 使用指定分隔符,拼接字符串
     *
     * @param delimiter 分隔符
     * @param strs      需要拼接的多个字符串,可以为null
     * @return 拼接后的新字符串
     */
    public static String joinStr(String delimiter,String... strs) {
        if (ObjectUtils.isEmpty(strs)) {
            return StrUtil.EMPTY;
        }
        StringBuilder sbd = new StringBuilder();
        for (int i = 0; i < strs.length; i++) {
            if (ObjectUtils.isEmpty(strs[i])) {
                continue;
            }
            sbd.append(strs[i].trim());
            if (!ObjectUtils.isEmpty(sbd) && i < strs.length - 1 && !ObjectUtils.isEmpty(strs[i + 1])) {
                sbd.append(delimiter);
            }
        }
        return sbd.toString();
    }
}

request流只能获取一次

如果接口是用@RequestBody来接受数据,在拦截器中需要使用读取request的输入流 ,但ServletRequestgetReader()getInputStream()只能调用一次

@ConditionalOnBean(SignatureProps.class)
@Component
public class RequestCachingFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestWrapper = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestWrapper = new ContentCachingRequestWrapper(request);
        }
        filterChain.doFilter(requestWrapper, response);
    }
}

测试类

@RestController
public class controller {

    @Resource
    private SignatureManager signatureManager;


    /**
     * 数据验签
    * 参数:{"name":"一安未来"}
    **/
    @VerifySignature(resubmit = false)
    @PostMapping("/sign1")
    public String sign1(@RequestBody Map<String,String> map){
        System.out.println(map);
        return "success";
    }

    /**
     *  数据签名
     * 参数:{"name":"一安未来"}
     **/
    @PostMapping("/sign2")
    public String sign2(@RequestBody Map<String,String> map, HttpServletRequest httpRequest){
        String clientID = httpRequest.getHeader("clientID");
        String mapToJsonString = String.format("{%s}", String.join(",", map.entrySet().stream()
                .map(e -> String.format("\"%s\":\"%s\"", e.getKey(), e.getValue()))
                .collect(Collectors.toList())));
        return signatureManager.sign(clientID,mapToJsonString);
    }
}

一安未来

致力于Java,大数据;心得交流,技术分享;

122篇原创内容

公众号

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RSA数字签名是非常重要的密算法之一,以下是Java实现RSA数字签名的代码示例: ## 数字签名 ```java import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.security.Key; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; public class RSAUtil { private static final String ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1WithRSA"; /** * 从文件中载私钥 * * @param privateKeyFile 私钥文件 * @return 私钥 * @throws Exception */ public static PrivateKey loadPrivateKeyFromFile(File privateKeyFile) throws Exception { FileInputStream fis = new FileInputStream(privateKeyFile); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { bos.write(buffer, 0, len); } fis.close(); bos.close(); byte[] privateKeyBytes = bos.toByteArray(); PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePrivate(pkcs8KeySpec); } /** * 从文件中载公钥 * * @param publicKeyFile 公钥文件 * @return 公钥 * @throws Exception */ public static PublicKey loadPublicKeyFromFile(File publicKeyFile) throws Exception { FileInputStream fis = new FileInputStream(publicKeyFile); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { bos.write(buffer, 0, len); } fis.close(); bos.close(); byte[] publicKeyBytes = bos.toByteArray(); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePublic(x509KeySpec); } /** * 对数据进行数字签名 * * @param data 数据 * @param privateKey 私钥 * @return 数字签名 * @throws Exception */ public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception { Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initSign(privateKey); signature.update(data); return signature.sign(); } /** * 验证数字签名 * * @param data 数据 * @param publicKey 公钥 * @param signedData 数字签名 * @return 是否验证通过 * @throws Exception */ public static boolean verify(byte[] data, PublicKey publicKey, byte[] signedData) throws Exception { Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initVerify(publicKey); signature.update(data); return signature.verify(signedData); } } ``` ## 使用示例 ```java import java.io.File; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.PublicKey; public class Main { public static void main(String[] args) throws Exception { // 载私钥 File privateKeyFile = new File("private_key.txt"); PrivateKey privateKey = RSAUtil.loadPrivateKeyFromFile(privateKeyFile); // 载公钥 File publicKeyFile = new File("public_key.txt"); PublicKey publicKey = RSAUtil.loadPublicKeyFromFile(publicKeyFile); // 待签名数据 String data = "Hello, World!"; byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); // 数字签名 byte[] signedData = RSAUtil.sign(dataBytes, privateKey); // 验证数字签名 boolean verified = RSAUtil.verify(dataBytes, publicKey, signedData); System.out.println("Verified: " + verified); } } ``` 以上代码示例中,我们使用了`RSAUtil`类中的`loadPrivateKeyFromFile`方法和`loadPublicKeyFromFile`方法分别从私钥文件和公钥文件中载私钥和公钥。然后我们使用`sign`方法对待签名数据进行数字签名,使用`verify`方法对签名结果进行验证。 切记,要使用自己的密钥对,不要使用他人的密钥对。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值