SpringBoot 請求消息體解密(通信加密解密)

介紹

在一些安全性要求較高的項(xiàng)目中,我們希望客戶端請求數(shù)據(jù)可以做到數(shù)據(jù)加密,服務(wù)器端進(jìn)行解密。(單純的HTTPS仍難以滿足安全需要。)

本文基于SpringBoot針對消息體進(jìn)行解密,目前僅支持請求消息解密。(響應(yīng)消息過大情況下,加密會(huì)帶來嚴(yán)重的性能問題。)

流程如下:
使用DES cbc模式對稱加密請求體。要求客戶端請求前加對消息體進(jìn)行加密,服務(wù)器端通過SpringMVC Advice攔截請求解密后,傳給controller的方法。

@ControllerAdvice與RequestBodyAdviceAdapter

@ControllerAdvice注解可以掃描針對Controller層的擴(kuò)展組件。通過@Sort注解可以使其支持順序加載。
RequestBodyAdviceAdapter是RequestBodyAdvice適配器類,可以方便的擴(kuò)展所需要的方法。

RequestBodyAdvice功能如下:
允許在請求消息體在被讀取及調(diào)用convert轉(zhuǎn)換成實(shí)體之前做一些個(gè)人化操作,作用于含有@RequestBody注解的請求。實(shí)現(xiàn)此接口的類,需要在RequestMappingHandlerAdapter中配置或通過@ControllerAdvice注解配置。

原文如下:

/**
 * Allows customizing the request before its body is read and converted into an
 * Object and also allows for processing of the resulting Object before it is
 * passed into a controller method as an {@code @RequestBody} or an
 * {@code HttpEntity} method argument.
 *
 * <p>Implementations of this contract may be registered directly with the
 * {@code RequestMappingHandlerAdapter} or more likely annotated with
 * {@code @ControllerAdvice} in which case they are auto-detected.
 *
 * @author Rossen Stoyanchev
 * @since 4.2
 */

完整代碼如下:

SecretRequestAdvice

@Slf4j
@ControllerAdvice
@ConditionalOnProperty(prefix = "faster.secret", name = "enabled", havingValue = "true")
@EnableConfigurationProperties({SecretProperties.class})
@Order(1)
public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
    @Autowired
    private SecretProperties secretProperties;


    /**
     * 是否支持加密消息體
     *
     * @param methodParameter methodParameter
     * @return true/false
     */
    private boolean supportSecretRequest(MethodParameter methodParameter) {
        if (!secretProperties.isScanAnnotation()) {
            return true;
        }
        //判斷class是否存在注解
        if (methodParameter.getContainingClass().getAnnotation(secretProperties.getAnnotationClass()) != null) {
            return true;
        }
        //判斷方法是否存在注解
        return methodParameter.getMethodAnnotation(secretProperties.getAnnotationClass()) != null;
    }


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

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        //如果支持加密消息,進(jìn)行消息解密。
        boolean supportSafeMessage = supportSecretRequest(parameter);
        String httpBody;
        if (supportSafeMessage) {
            httpBody = decryptBody(inputMessage);
            if (httpBody == null) {
                throw new HttpMessageNotReadableException("request body decrypt error");
            }
        } else {
            httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
        }
        //返回處理后的消息體給messageConvert
        return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
    }

    /**
     * 解密消息體,3des解析(cbc模式)
     *
     * @param inputMessage 消息體
     * @return 明文
     */
    private String decryptBody(HttpInputMessage inputMessage) throws IOException {
        InputStream encryptStream = inputMessage.getBody();
        String encryptBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
        return DesCbcUtil.decode(encryptBody, secretProperties.getDesSecretKey(), secretProperties.getDesIv());
    }
}

SecretBody

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SecretBody {
}

SecretHttpMessage

@AllArgsConstructor
@NoArgsConstructor
public class SecretHttpMessage implements HttpInputMessage {
    private InputStream body;
    private HttpHeaders httpHeaders;

    @Override
    public InputStream getBody() {
        return this.body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return this.httpHeaders;
    }
}

SecretProperties

@ConfigurationProperties(prefix = "faster.secret")
@Data
public class SecretProperties {
    /**
     * 是否開啟
     */
    private boolean enabled;
    /**
     * 是否掃描注解
     */
    private boolean scanAnnotation;
    /**
     * 掃描自定義注解
     */
    private Class<? extends Annotation> annotationClass = SecretBody.class;

    /**
     * 3des 密鑰長度不得小于24
     */
    private String desSecretKey = "b2c17b46e2b1415392aab5a82869856c";
    /**
     * 3des IV向量必須為8位
     */
    private String desIv = "61960842";

}

DesCbcUtil

@Slf4j
public class DesCbcUtil {
    // 加解密統(tǒng)一使用的編碼方式
    private final static String encoding = "UTF-8";

    /**
     * 3DES加密
     *
     * @param plainText 普通文本
     * @return 加密后的文本,失敗返回null
     */
    public static String encode(String plainText, String secretKey, String iv) {
        String result = null;
        try {
            DESedeKeySpec deSedeKeySpec = new DESedeKeySpec(secretKey.getBytes());
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("desede");
            Key desKey = secretKeyFactory.generateSecret(deSedeKeySpec);
            Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
            IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
            cipher.init(Cipher.ENCRYPT_MODE, desKey, ips);
            byte[] encryptData = cipher.doFinal(plainText.getBytes(encoding));
            result = Base64Utils.encodeToString(encryptData);
        } catch (Exception e) {
            log.error("DesCbcUtil encode error : {}", e);
        }
        return result;
    }

    /**
     * 3DES解密
     *
     * @param encryptText 加密文本
     * @return 解密后明文,失敗返回null
     */
    public static String decode(String encryptText, String secretKey, String iv) {
        String result = null;
        try {
            DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes());
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("desede");
            Key desKey = secretKeyFactory.generateSecret(spec);
            Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
            IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, desKey, ips);
            byte[] decryptData = cipher.doFinal(Base64Utils.decodeFromString(encryptText));
            result = new String(decryptData, encoding);
        } catch (Exception e) {
            log.error("DesCbcUtil decode error : {}", e.getMessage());
        }
        return result;
    }

}

使用方式

使用以下注解即可快速開啟全部請求的服務(wù)器端消息體解密功能。

faster:
  secret:
    enabled: true

局部解密

使用scan-annotation可開啟注解所標(biāo)注的Conrtoller的類或其方法的解密功能。將要解密的方法或類上添加@SecretBody注解。并開啟以下配置:

faster:
  secret:
    enabled: true
    scan-annotation: true

可以使用annotation-class配置自己的自定義注解:

faster:
  secret:
    enabled: true
    scan-annotation: true
    annotation-class: cn.test.xxx

注解使用

作用于整個(gè)類:

@SecretBody
public class DemoController {

}

作用于方法:


public class DemoController {
   @PostMapping("secretBody")
   @SecretBody
    public int secretBody(@RequestBody UserEntity userEntity) {
        log.info("{}", userEntity);
        return 0;
    }
}

密鑰

默認(rèn)密鑰如下,可以自行修改

faster:
  secret:
    enabled: true
    des-secret-key: b2c17b46e2b1415392aab5a82869856c
    des-iv: 61960842

前端調(diào)用

前端調(diào)用時(shí),需先將要請求的消息體通過DEScbc模式加密消息體(如json字符串)后傳輸。一般在http工具的請求攔截器中進(jìn)行處理。如為json,仍然需要指定content-type為application/json。
postman請求示例如下:

image.png

快速開發(fā)框架
高質(zhì)量圖片壓縮工具

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容