外卖CPS系统中Java后端服务的接口加密与数据脱敏实现技巧

在外卖CPS系统中,涉及用户手机号、银行卡号、佣金明细等敏感数据,需在传输和存储环节实施强加密与动态脱敏。本文结合AES/GCM对称加密、RSA非对称密钥交换、Jackson序列化脱敏、Spring AOP拦截等技术,提供端到端的安全防护方案。

1. 请求/响应体自动加解密(基于Spring MVC拦截)

自定义RequestBodyAdviceResponseBodyAdvice

package baodanbao.com.cn.cps.security;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.io.ByteArrayInputStream;
import java.lang.reflect.Type;

@ControllerAdvice
public class CryptoResponseBodyAdvice implements ResponseBodyAdvice<Object> {

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

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  Type selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Encryptable) {
            String json = JSON.toJSONString(body);
            String encrypted = AesGcmUtil.encrypt(json, getSecretKeyFromHeader(request));
            return new EncryptedResponse(encrypted);
        }
        return body;
    }
}

// 请求解密
public class CryptoRequestBodyAdvice extends RequestBodyAdviceAdapter {

    @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 {
        if (isEncryptedRequest(parameter)) {
            String encrypted = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
            String secretKey = getSecretKeyFromHeader(inputMessage.getHeaders());
            String plainJson = AesGcmUtil.decrypt(encrypted, secretKey);
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() {
                    return new ByteArrayInputStream(plainJson.getBytes(StandardCharsets.UTF_8));
                }
                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        }
        return inputMessage;
    }
}

在这里插入图片描述

2. AES-GCM加解密工具类(认证加密)

public class AesGcmUtil {

    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 16;

    public static String encrypt(String plaintext, String base64Key) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
            SecretKey key = new SecretKeySpec(keyBytes, "AES");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, generateIv());
            cipher.init(Cipher.ENCRYPT_MODE, key, spec);
            byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            byte[] output = new byte[GCM_IV_LENGTH + ciphertext.length];
            System.arraycopy(spec.getIV(), 0, output, 0, GCM_IV_LENGTH);
            System.arraycopy(ciphertext, 0, output, GCM_IV_LENGTH, ciphertext.length);
            return Base64.getEncoder().encodeToString(output);
        } catch (Exception e) {
            throw new RuntimeException("Encrypt failed", e);
        }
    }

    public static String decrypt(String encryptedBase64, String base64Key) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
            byte[] data = Base64.getDecoder().decode(encryptedBase64);
            byte[] iv = Arrays.copyOf(data, GCM_IV_LENGTH);
            byte[] ciphertext = Arrays.copyOfRange(data, GCM_IV_LENGTH, data.length);
            SecretKey key = new SecretKeySpec(keyBytes, "AES");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            byte[] plaintext = cipher.doFinal(ciphertext);
            return new String(plaintext, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("Decrypt failed", e);
        }
    }

    private static byte[] generateIv() {
        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
}

3. 敏感字段动态脱敏(基于Jackson注解)

自定义@Sensitive注解与序列化器:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveDataSerializer.class)
public @interface Sensitive {
    SensitiveType value() default SensitiveType.MOBILE;
}

public enum SensitiveType {
    MOBILE, ID_CARD, BANK_CARD
}

public class SensitiveDataSerializer extends JsonSerializer<String> {

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        SensitiveType type = findSensitiveType(serializers);
        String masked = switch (type) {
            case MOBILE -> value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
            case ID_CARD -> value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
            case BANK_CARD -> value.replaceAll("(\\d{4})\\d{8}(\\d{4})", "$1********$2");
            default -> value;
        };
        gen.writeString(masked);
    }

    private SensitiveType findSensitiveType(SerializerProvider provider) {
        // 从上下文或字段元数据获取类型,简化处理
        return SensitiveType.MOBILE;
    }
}

使用示例:

public class UserInfo {
    private String name;
    
    @Sensitive(SensitiveType.MOBILE)
    private String phone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard;
}

4. 数据库存储加密(MyBatis TypeHandler)

对银行卡号等字段透明加解密:

@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptedStringTypeHandler extends BaseTypeHandler<String> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        String encrypted = AesGcmUtil.encrypt(parameter, getKeyFromConfig());
        ps.setString(i, encrypted);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String encrypted = rs.getString(columnName);
        return encrypted != null ? AesGcmUtil.decrypt(encrypted, getKeyFromConfig()) : null;
    }

    private String getKeyFromConfig() {
        return baodanbao.com.cn.cps.config.SecurityConfig.getDbEncryptKey();
    }
}

Mapper中指定:

<result column="bank_card" property="bankCard" typeHandler="baodanbao.com.cn.cps.security.EncryptedStringTypeHandler"/>

5. 密钥安全管理

禁止硬编码,通过KMS或配置中心动态获取:

@Component
public class KeyManager {

    private final String kmsUrl;
    private volatile String cachedKey;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    @PostConstruct
    public void init() {
        refreshKey();
        scheduler.scheduleAtFixedRate(this::refreshKey, 1, 1, TimeUnit.HOURS);
    }

    private void refreshKey() {
        ResponseEntity<String> resp = restTemplate.getForEntity(kmsUrl + "/key/cps-data", String.class);
        this.cachedKey = resp.getBody(); // Base64格式
    }

    public String getCurrentKey() {
        return cachedKey;
    }
}

本文著作权归 俱美开放平台 ,转载请注明出处!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐