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<Object> {

    @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<String, Object> 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实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 142,272评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,027评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 93,908评论 0 212
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,909评论 0 175
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,638评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,710评论 1 175
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,332评论 2 267
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,125评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,938评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,475评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,244评论 2 214
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,566评论 1 227
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,206评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,095评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,479评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,609评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,988评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,480评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,549评论 2 229