SpringMvc/SpringBoot HTTP通信加解密

来源:互联网 发布:电力猫网络dns不正常 编辑:程序博客网 时间:2024/05/29 09:16

前言

从去年10月份到现在忙的没时间写博客了,今天就甩给大家一个干货吧!!!

近来很多人问到下面的问题

  1. 我们不想在每个Controller方法收到字符串报文后再调用一次解密,虽然可以完成,但是很low,且如果想不再使用加解密,修改起来很是麻烦。
  2. 我们想在使用Rest工具或swagger请求的时候不进行加解密,而在app调用的时候处理加解密,这可如何操作。

针对以上的问题,下面直接给出解决方案:

实现思路

  1. APP调用API的时候,如果需要加解密的接口,需要在httpHeader中给出加密方式,如header[encodeMethod]
  2. Rest工具或swagger请求的时候无需指定此header。
  3. 后端API收到request后,判断header中的encodeMethod字段,如果有值,则认为是需要解密,否则就认为是明文。

约定

为了精简分享技术,先约定只处理POST上传JSON(application/json)数据的加解密处理

请求解密实现方式

1. 先定义controller

@Controller@RequestMapping("/api/demo")public class MyDemoController {    @RequestDecode    @ResponseBody    @RequestMapping(value = "user", method = RequestMethod.POST)    public ResponseDto addUser(            @RequestBody User user    ) throws Exception {        //TODO ...    }}
/** * 解密请求数据 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestDecode {    SecurityMethod method() default SecurityMethod.NULL;}

可以看到这里的Controller定义的很普通,只有一个额外的自定义注解RequestDecode,这个注解是为了下面的RequestBodyAdvice的使用。

2. 建设自己的RequestBodyAdvice

有了上面的入口定义,接下来处理解密这件事,目的很明确:
1. 是否需要解密判断httpHeader中的encodeMethod字段。
2. 在进入controller之前就解密完成,是controller处理逻辑无感知。

DecodeRequestBodyAdvice.java

@Slf4j@Component@ControllerAdvice(basePackages = "com.xxx.hr.api.controller")public class DecodeRequestBodyAdvice implements RequestBodyAdvice {    @Value("${hrapi.aesKey}")    String aesKey;    @Value("${hrapi.googleKey}")    String googleKey;    @Override    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {        return methodParameter.getMethodAnnotation(RequestDecode.class) != null            && methodParameter.getParameterAnnotation(RequestBody.class) != null;    }    @Override    public Object handleEmptyBody(Object body, HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {        return body;    }    @Override    public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {        RequestDecode requestDecode = parameter.getMethodAnnotation(RequestDecode.class);        if (requestDecode == null) {            return request;//controller方法不要求加解密        }        String appId = request.getHeaders().getFirst(com.xxx.hr.bean.constant.HttpHeaders.APP_ID);//这里是扩展,可以知道来源方(如开放平台使用)        String encodeMethod = request.getHeaders().getFirst(com.xxx.hr.bean.constant.HttpHeaders.ENCODE_METHOD);         if (StringUtils.isEmpty(encodeMethod)) {            return request;        }        SecurityMethod encodeMethodEnum = SecurityMethod.getByCode(encodeMethod);         //这里灵活的可以支持到多种加解密方式        switch (encodeMethodEnum) {            case NULL:                break;            case AES: {                InputStream is = request.getBody();                ByteBuf buf = PooledByteBufAllocator.DEFAULT.heapBuffer();                int ret = -1;                int len = 0;                while((ret = is.read()) > 0) {                    buf.writeByte(ret);                    len ++;                }                String body = buf.toString(0, len, xxxSecurity.DEFAULT_CHARSET);                buf.release();                String temp = null;                try {                    temp = XxxSecurity.aesDecodeData(body, aesKey, googleKey, new CheckCallBack() {                        @Override                        public boolean isRight(String data) {                            return data != null && (data.startsWith("{") || data.startsWith("["));                        }                    });                    log.info("解密完成: {}", temp);                    return new DecodedHttpInputMessage(request.getHeaders(), new ByteArrayInputStream(temp.getBytes("UTF-8")));                } catch (DecodeException e) {                    log.warn("解密失败 appId: {}, Name:{} 待解密密文: {}", appId, partnerName, body, e);                    throw e;                }            }        }        return request;    }    @Override    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {        return body;    }    static class DecodedHttpInputMessage implements HttpInputMessage {        HttpHeaders headers;        InputStream body;        public DecodedHttpInputMessage(HttpHeaders headers, InputStream body) {            this.headers = headers;            this.body = body;        }        @Override        public InputStream getBody() throws IOException {            return body;        }        @Override        public HttpHeaders getHeaders() {            return headers;        }    }}

至此加解密完成了。


————————-华丽分割线 —————————–


响应加密

下面附件一下响应加密过程,目的
1. Controller逻辑代码无感知
2. 可以一键开关响应加密

定义Controller

    @ResponseEncode    @ResponseBody    @RequestMapping(value = "employee", method = RequestMethod.GET)    public ResponseDto<UserEEInfo> userEEInfo(            @ApiParam("用户编号") @RequestParam(HttpHeaders.APPID) Long userId    ) {        //TODO ...    }
/** * 加密响应数据 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ResponseEncode {    SecurityMethod method() default SecurityMethod.NULL;}

这里的Controller定义的也很普通,只有一个额外的自定义注解ResponseEncode,这个注解是为了下面的ResponseBodyAdvice的使用。

建设自己的ResponseBodyAdvice

这里约定将响应的DTO序列化为JSON格式数据,然后再加密,最后在响应给请求方。

@Slf4j@Component@ControllerAdvice(basePackages = "com.xxx.hr.api.controller")public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {    @Autowired    PartnerService partnerService;    @Override    public boolean supports(MethodParameter returnType, Class converterType) {        return returnType.getMethodAnnotation(ResponseEncode.class) != null;    }    @Override    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {        ResponseEncode responseEncode = returnType.getMethodAnnotation(ResponseEncode.class);        String uid = request.getHeaders().getFirst(HttpHeaders.PARTNER_UID);        if (uid == null) {            uid = request.getHeaders().getFirst(HttpHeaders.APP_ID);        }        PartnerConfig config = partnerService.getConfigByAppId(uid);        if (responseEncode.method() == SecurityMethod.NULL || responseEncode.method() == SecurityMethod.AES) {            if (config == null) {                return ResponseDto.rsFail(ResponseCode.E_403, "商户不存在");            }            String temp = JSON.toJSONString(body);            log.debug("待加密数据: {}", temp);            String encodedBody = MLJRSecurity.aesEncodeData(temp, config.getEncryptionKey(), config.getGoogleKey());            log.debug("加密完成: {}", encodedBody);            response.getHeaders().set(HttpHeaders.ENCODE_METHOD, HttpHeaders.VALUE.AES);            response.getHeaders().set(HttpHeaders.HEADER_CONTENT_TYPE, HttpHeaders.VALUE.APPLICATION_BASE64_JSON_UTF8);            response.getHeaders().remove(HttpHeaders.SIGN_METHOD);            return encodedBody;        }        return body;    }}

拓展

由上面的实现,如何实现RSA验证签名呢?这个就简单了,请看分解。

目的还是很简单,进来减少对业务逻辑的入侵。

首先设定一下那些请求需要验证签名

    @RequestSign    @ResponseEncode    @ResponseBody    @RequestMapping(value = "employee", method = RequestMethod.GET)    public ResponseDto<UserEEInfo> userEEInfo(            @ApiParam("用户编号") @RequestParam(HttpHeaders.UID) String uid    ) {        //TODO ...    }

这里还是使用一个注解RequestSign,然后再实现一个SignInterceptor即可完成:

@Slf4j@Componentpublic class SignInterceptor implements HandlerInterceptor {    @Autowired    PartnerService partnerService;    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        HandlerMethod method = (HandlerMethod) handler;        RequestSign requestSign = method.getMethodAnnotation(RequestSign.class);        if (requestSign == null) {            return true;        }        String appId = request.getHeader(HttpHeaders.APP_ID);        ValidateUtils.notTrimEmptyParam(appId, "Header[appId]");        PartnerConfig config = partnerService.getConfigByAppId(appId);        ValidateUtils.notNull(config, Code.E_400, "商戶不存在");        String partnerName = partnerService.getPartnerName(appId);        String sign = request.getParameter(HttpHeaders.SIGN);        String signMethod = request.getParameter(HttpHeaders.SIGN_METHOD);        signMethod = (signMethod == null) ? "RSA" : signMethod;        Map<String, String[]> parameters = request.getParameterMap();        ValidateUtils.notTrimEmptyParam(sign, "sign");        if ("RSA".equals(signMethod)) {            sign = sign.replaceAll(" ", "+");            boolean isOK = MLJRSecurity.signVerifyRequest(parameters, config.getRsaPublicKey(), sign, config.getSecurity());            if (isOK) {                log.info("验证商户签名通过 {}[{}] ", appId, partnerName);                return true;            } else {                log.warn("验证商户签名失败 {}[{}] ", appId, partnerName);            }        } else {            throw new SignVerifyException("暂不支持该签名");        }        throw new SignVerifyException("签名校验失败");    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {    }}
原创粉丝点击