Api接口加解密

本文介绍了如何在SpringBoot项目中实现客户端请求数据的加密和服务器端的解密,包括配置文件设置、自定义注解控制加密行为、不同请求方式(表单和JSONbody)的参数处理以及AES加密/解密的实现原理。
摘要由CSDN通过智能技术生成

背景

在一些安全性要求较高的项目中,我们希望客户端请求数据可以做到数据加密,服务器端进行解密。单纯的 HTTPS 仍难以满足安全需要。

快速上手

application.properties配置文件

# 是否启用,默认false
security.api.encrypt.enabled=true
# 秘钥
security.api.encrypt.secret-key=自定义(16位)
# 是否开启全局加解密,默认false
security.api.encrypt.global-enabled=false

自定义加解密注解

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiSecret {
    // 是否忽略加解密
    boolean ignore() default false;
}

请求方式

普通表单

@PostMapping("/xxx")
public Result<String> form(@RequestParam(required = false) String param1,
                           @RequestParam(required = false) Integer param2) {
    return new Result<String>().ok();
}

在这里插入图片描述

普通json body

@PostMapping("/xxx")
public Result<String> body(@RequestBody T t) {
    return new Result<String>().ok();
}

在这里插入图片描述

实现原理

@Data
@Component
@ConfigurationProperties(ApiSecretProperties.PREFIX)
public class ApiSecretProperties {

    /**
     * 前缀
     */
    public static final String PREFIX = "security.api.encrypt";

    /**
     * 固定参数名称
     */
    public static final String PARAM_NAME = "encryption";

    /**
     * 是否启用标志,默认false
     */
    private boolean enabled = false;

    /**
     * 是否全局加解密,默认false
     */
    private boolean globalEnabled = false;

    /**
     * 秘钥
     */
    private String secretKey;
}
public class ApiSecretSupports {
    private static final Set<String> IGNORES = new HashSet<>();
    static {
        IGNORES.add("springfox.documentation.swagger.web.ApiResourceController");
        IGNORES.add("springfox.documentation.swagger2.web.Swagger2Controller");
        IGNORES.add("org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController");
    }

    public static boolean supports(MethodParameter parameter, ApiSecretProperties properties) {
        if (IGNORES.contains(parameter.getDeclaringClass().getName())) {
            return false;
        }

        return supports(parameter, properties.isGlobalEnabled());
    }

    private static boolean supports(MethodParameter parameter, boolean isGlobalEnabled) {
        ApiSecret cApiSecret = parameter.getContainingClass().getAnnotation(ApiSecret.class);
        if (cApiSecret == null) {
            ApiSecret apiSecret = parameter.getMethodAnnotation(ApiSecret.class);
            if (apiSecret == null) {
                return isGlobalEnabled;
            }

            return !apiSecret.ignore();
        }

        if (cApiSecret.ignore()) {
            return false;
        }

        ApiSecret apiSecret = parameter.getMethodAnnotation(ApiSecret.class);
        if (apiSecret == null) {
            return true;
        }

        return !apiSecret.ignore();
    }
}
@Getter
@RequiredArgsConstructor
public class ApiSecretHttpInputMessage implements HttpInputMessage {
    private final InputStream body;
    private final HttpHeaders headers;
}

调整自定义参数解析器的优先级

@Component
@RequiredArgsConstructor
@ConditionalOnBean(ApiSecretRequestParamResolver.class)
public class ApiSecretResolverBeanPostProcessor implements BeanPostProcessor {

    private final ApiSecretRequestParamResolver requestParamResolver;
    private static final String HANDLER_ADAPTER = "requestMappingHandlerAdapter";

    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, @Nullable String beanName) throws BeansException {
        if (HANDLER_ADAPTER.equals(beanName)) {
            RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
            if (adapter != null) {
                List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
                argumentResolvers.add(requestParamResolver);
                if (CollectionUtils.isNotEmpty(adapter.getArgumentResolvers())) {
                    argumentResolvers.addAll(adapter.getArgumentResolvers());
                }

                adapter.setArgumentResolvers(argumentResolvers);
            }
        }

        return bean;
    }
}

@RequestParam参数解密

@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(value = ApiSecretProperties.PREFIX + ".enabled", havingValue = "true")
@ConditionalOnExpression("!'${security.api.encrypt.secret-key}'.isEmpty()")
public class ApiSecretRequestParamResolver implements HandlerMethodArgumentResolver {

    private final ObjectMapper objectMapper;
    private final ApiSecretProperties properties;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Method method = parameter.getMethod();
        if (!precondition(method)) {
            return false;
        }

        return ApiSecretSupports.supports(parameter, properties);
    }

    @Override
    @SneakyThrows
    @SuppressWarnings("unchecked")
    public Object resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
        Parameter parameter = methodParameter.getParameter();
        String params = webRequest.getParameter(ApiSecretProperties.PARAM_NAME);
        if (StringUtils.isBlank(params)) {
            return convertIfNecessary(methodParameter, webRequest, binderFactory);
        }

        AES aes = new AES(Mode.CBC, Padding.ZeroPadding, new SecretKeySpec(properties.getSecretKey().getBytes(), "AES"),
                new IvParameterSpec(properties.getSecretKey().getBytes()));
        try {
            params = aes.decryptStr(params);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new BusinessException("Please check if the encryption parameter value has been encrypted for transmission");
        }

        Map<String, Object> map = objectMapper.readValue(params, Map.class);
        Object o = map.get(parameter.getName());
        if (o == null) {
            return convertIfNecessary(methodParameter, webRequest, binderFactory);
        }
        return o;
    }

    private Object convertIfNecessary(MethodParameter methodParameter, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Object o = null;
        Parameter parameter = methodParameter.getParameter();
        RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
        String defaultValue = (ValueConstants.DEFAULT_NONE.equals(requestParam.defaultValue()) ? null : requestParam.defaultValue());
        if (StringUtils.isNotBlank(defaultValue)) {
            o =  defaultValue;
        } else if (requestParam.required()) {
            throw new MissingServletRequestParameterException(parameter.getName(),
                    parameter.getType().getSimpleName(), false);
        }

        o = handleNullValue(parameter.getName(), o, parameter.getType());
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getName());
            try {
                o = binder.convertIfNecessary(o, methodParameter.getParameterType(), methodParameter);
            } catch (ConversionNotSupportedException ex) {
                throw new MethodArgumentConversionNotSupportedException(o, ex.getRequiredType(),
                        parameter.getName(), methodParameter, ex.getCause());
            } catch (TypeMismatchException ex) {
                throw new MethodArgumentTypeMismatchException(o, ex.getRequiredType(),
                        parameter.getName(), methodParameter, ex.getCause());
            }

            if (o == null && defaultValue == null && requestParam.required()) {
                throw new MissingServletRequestParameterException(parameter.getName(),
                        parameter.getType().getSimpleName(), true);
            }
        }

        return o;
    }


    private Object handleNullValue(String name, @Nullable Object o, Class<?> paramType) {
        if (o == null) {
            if (Boolean.TYPE.equals(paramType)) {
                return Boolean.FALSE;
            }
            else if (paramType.isPrimitive()) {
                throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name +
                        "' is present but cannot be translated into a null value due to being declared as a " +
                        "primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
            }
        }

        return o;
    }

    private boolean precondition(Method method) {
        List<Parameter> parameters = Arrays.stream(Objects.requireNonNull(method).getParameters()).filter(p -> !p.isAnnotationPresent(RequestParam.class))
                .collect(Collectors.toList());
        return !CollectionUtils.isNotEmpty(parameters);
    }

}

@RequestBody参数解密

@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
@ConditionalOnProperty(value = ApiSecretProperties.PREFIX + ".enabled", havingValue = "true")
@ConditionalOnExpression("!'${security.api.encrypt.secret-key}'.isEmpty()")
public class ApiSecretRequestBodyAdvice implements RequestBodyAdvice {

    private final ObjectMapper objectMapper;
    private final ApiSecretProperties properties;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return ApiSecretSupports.supports(methodParameter, properties);
    }

    @Override
    @SneakyThrows
    @SuppressWarnings("unchecked")
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        InputStream messageBody = inputMessage.getBody();
        byte[] bodyByteArray = StreamUtils.copyToByteArray(messageBody);
        Map<String, Object> dataMap = objectMapper.readValue(bodyByteArray, Map.class);
        String content = (String) dataMap.get(ApiSecretProperties.PARAM_NAME);
        Preconditions.checkArgument(StringUtils.isNotBlank(content), "The encryption parameter requires non empty values");

        AES aes = new AES(Mode.CBC, Padding.ZeroPadding, new SecretKeySpec(properties.getSecretKey().getBytes(), "AES"),
                new IvParameterSpec(properties.getSecretKey().getBytes()));

        byte[] decryptedBody;
        try {
            decryptedBody = aes.decrypt(StrUtil.str(content.getBytes(StandardCharsets.UTF_8), Charset.defaultCharset()));
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new BusinessException("Please check if the encryption parameter value has been encrypted for transmission");
        }

        InputStream inputStream = new ByteArrayInputStream(decryptedBody);
        return new ApiSecretHttpInputMessage(inputStream, inputMessage.getHeaders());
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

响应数据加密

@ControllerAdvice
@RequiredArgsConstructor
@ConditionalOnProperty(value = ApiSecretProperties.PREFIX + ".enabled", havingValue = "true")
@ConditionalOnExpression("!'${security.api.encrypt.secret-key}'.isEmpty()")
public class ApiSecretResponseBodyAdvice implements ResponseBodyAdvice<ResponseEntity<Object>> {

    private final ObjectMapper objectMapper;
    private final ApiSecretProperties properties;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return ApiSecretSupports.supports(returnType, properties);
    }

    @Override
    @SneakyThrows
    public ResponseEntity<Object> beforeBodyWrite(@Nullable Result<Object> responseEntity, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (responseEntity == null || responseEntity.getData() == null) {
            return responseEntity;
        }

        byte[] bytes = objectMapper.writeValueAsBytes(responseEntity.getData());
        AES aes = new AES(Mode.CBC, Padding.ZeroPadding, new SecretKeySpec(properties.getSecretKey().getBytes(), "AES"),
                new IvParameterSpec(properties.getSecretKey().getBytes()));
        return responseEntity.data(aes.encryptBase64(bytes));
    }
}

总结

接口加解密是确保数据安全的关键技术之一,其通过对传输中的数据进行加密来防止数据在传递过程中被拦截、篡改或伪造。但是,单靠加解密技术并不足以应对所有安全挑战。必须整合多种安全策略,建立一个全方位的安全防护体系,以确保接口的全面安全。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丿微风乍起

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值