Springboot统一参数解密的一些方案

在项目开发中,很多场景下我们的接口参数都需要进行加解密处理。

例如我开发的此项目中,参数原文使用AES加密为 params 字段,AES key使用RSA加密为 encKey 字段。

不统一处理

如果不对参数解密进行统一处理

@PostMapping("login")
public ResponseEntity login(HttpServletRequest request){
    String params = request.getParameter("params");
    String encKey = request.getParameter("encKey");
    
    /* 1.RSA私钥解密出AES的KEY */
    PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
    aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey), privateKey);

    /* 2.AES解密出原始参数 */
    String decrypt = AESUtil.decrypt(params, new String(aesKey));
    JSONObject originParam = JSONObject.parseObject(decrypt);
}

则需在每个需参数解密的方法进行参数解密,代码冗余度较高

方案1:HttpServletRequestWrapper

  1. 定义一个HttpServletRequestWrapper,并在Wrapper中实现参数解密逻辑
@Slf4j
public class EncHttpServletRequest extends HttpServletRequestWrapper {

    private JSONObject originParam;

    private String encKey;

    private String params;

    public EncHttpServletRequest(HttpServletRequest request) throws GlobalException{
        super(request);

        String encKey = request.getParameter("encKey");
        String params = request.getParameter("params");

        this.encKey = encKey;
        this.params = params;

        if(StringUtils.isEmpty(encKey)||StringUtils.isEmpty(params)){
            throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM);
        }
        byte[] aesKey;
        try{
            /* 1.RSA私钥解密出AES的KEY */
            PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
            aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey), privateKey);

            /* 2.AES解密出原始参数 */
            String decrypt = AESUtil.decrypt(params, new String(aesKey));
            originParam = JSONObject.parseObject(decrypt);
        }catch (Exception e){
            e.printStackTrace();
            throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM_DECRYPT);
        }
    }
}
  1. 再定义一个Filter,对请求进行Wrap
@Slf4j
@Component
@Order
@WebFilter(urlPatterns = "/*", filterName = "paramsDecryptFilter")
public class ParamsDecryptFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest)servletRequest;
        HttpServletResponse rsp = (HttpServletResponse)servletResponse;
        // 对需要wrap的请求进行判断转换
        if(HttpMethod.POST.name().equals(req.getMethod())){
            EncHttpServletRequest encHttpServletRequest = null;
            try{
                encHttpServletRequest = new EncHttpServletRequest(req);
            }catch (GlobalException e){
                rsp.setContentType("application/json; charset=utf-8");
                try (PrintWriter writer = rsp.getWriter()) {
                    ResponseResult error = ResponseResult.error(e.getErrorEnum());
                    String errorResponse = JSONObject.toJSONString(error);
                    writer.write(errorResponse);
                }
                return;
            }
            filterChain.doFilter(encHttpServletRequest,rsp);
        }else{
            filterChain.doFilter(req,rsp);
        }
    }
}

这样,在特定情况下HttpServletRequest就会wrap成 EncHttpServletRequest,在endpoint中就可以直接使用:

@PostMapping("login")
public ResponseResult login(EncHttpServletRequest request){
    JSONObject originParam = request.getOriginParam();
}

但是这种方式对于参数体都没有直接在方法中明确,对于使用swagger或者其他接口文档生成拓展不是很友好。

方案2:AbstractHttpMessageConverter

  1. 实现AbstractHttpMessageConverter可以定义请求类型转换
/**
 * 定义加密http请求参数自定义类型转换
 * 当请求MediaType为application/x-www-form-urlencoded,@RequestBody参数为DecryptParam类型时自动转换
 */
@Slf4j
public class EncMessageConverter extends AbstractHttpMessageConverter {

    @Autowired
    private ObjectMapper objectMapper;

    public EncMessageConverter() {
        super(MediaType.APPLICATION_FORM_URLENCODED);
    }

    @Override
    protected boolean supports(Class clazz) {
        EncryptParam param = clazz.getAnnotation(EncryptParam.class);
        return param != null;
    }

    @Override
    protected Object readInternal(Class clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {

        // 解析原始加密参数
        Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
        String originBody = StreamUtils.copyToString(inputMessage.getBody(), charset);
        Map parameters = HttpParamUtil.getParameter("?"+originBody);

        Object encKey = parameters.get("encKey");
        Object params = parameters.get("params");

        if(StringUtils.isEmpty(encKey)||StringUtils.isEmpty(params)){
            throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM);
        }
        byte[] aesKey;
        try{
            /* 1.RSA私钥解密出AES的KEY */
            PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
            aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey.toString()), privateKey);

            /* 2.AES解密出原始参数 */
            String decrypt = AESUtil.decrypt(params.toString(), new String(aesKey));
            JavaType javaType = getJavaType(clazz, null);
            return this.objectMapper.readValue(decrypt.getBytes(), clazz);
        }catch (Exception e){
            e.printStackTrace();
            throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM_DECRYPT);
        }
    }

    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return super.canRead(clazz, mediaType);
    }

    @Override
    protected void writeInternal(Object request, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return false;
    }

    private Charset getContentTypeCharset(@Nullable MediaType contentType) {
        if (contentType != null && contentType.getCharset() != null) {
            return contentType.getCharset();
        }
        else if (contentType != null && contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
            // Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
            return StandardCharsets.UTF_8;
        }
        else {
            Charset charset = getDefaultCharset();
            Assert.state(charset != null, "No default charset");
            return charset;
        }
    }

    protected JavaType getJavaType(Type type, @Nullable Class contextClass) {
        TypeFactory typeFactory = this.objectMapper.getTypeFactory();
        return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
    }

    private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
        try {
            if (inputMessage instanceof MappingJacksonInputMessage) {
                Class deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
                if (deserializationView != null) {
                    return this.objectMapper.readerWithView(deserializationView).forType(javaType).
                            readValue(inputMessage.getBody());
                }
            }
            return this.objectMapper.readValue(inputMessage.getBody(), javaType);
        }
        catch (InvalidDefinitionException ex) {
            throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
        }
        catch (JsonProcessingException ex) {
            throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
        }
    }
}

 
 
  1. 创建参数注解
/**
 * 标识加密的参数type
 * EncMessageConverter进行自动类型转换
 * @see EncMessageConverter
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EncryptParam {

}

  1. 在原始参数对象上应用注解
@Data
@EncryptParam
public class LoginDTO {

    @NotEmpty
    private String username;

    @NotEmpty
    private String password;

    @NotEmpty
    private String timestamp;
}

这样一来,满足条件时,参数将进行自动类型转换,并且参数文档也能按照未加密时的原字段生成

@PostMapping("login")
public ResponseResult login(@RequestBody @Valid LoginDTO dto){

}

当然,也可以不使用AbstractHttpMessageConverter,自行定义注解完成AOP实现

你可能感兴趣的:(Springboot统一参数解密的一些方案)