首页 > 编程 > Java > 正文

SpringMvc/SpringBoot HTTP通信加解密的实现

2019-11-26 08:42:55
字体:
来源:转载
供稿:网友

前言

从去年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 = XxxSecurity.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(      @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 = xxxxSecurity.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 {  }}

各个枚举定义:

//加解密、签名算法枚举public enum SecurityMethod {  NULL,  AES,  RSA,  DES,  DES3,  SHA1,  MD5  ;}

注解定义:

/** * 请求数据数据需要解密 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestDecode {  SecurityMethod method() default SecurityMethod.NULL;}/** * 请求数据需要验签 */@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface RequestSign {  SecurityMethod method() default SecurityMethod.RSA;}/** * 数据响应需要加密 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ResponseEncode {  SecurityMethod method() default SecurityMethod.NULL;}/** * 响应数据需要生成签名 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface ResponseSign {  SecurityMethod method() default SecurityMethod.NULL;} 

aesDecodeData

/**   * AES 解密数据   *   * @param data       待解密数据   * @param aesKey      AES 密钥(BASE64)   * @param googleAuthKey   GoogleAuthKey(BASE64)   * @param originDataSign  原始数据md5签名   * @return   */  public static String aesDecodeDataEx(String data, String aesKey, String googleAuthKey, String originDataSign) {    return aesDecodeData(data, aesKey, googleAuthKey, System.currentTimeMillis(), null, originDataSign);  }  public static String aesDecodeData(String data, String aesKey, String googleAuthKey, long tm, CheckCallBack checkCallBack, String originDataSign) {    DecodeException lastError = null;    long timeWindow = googleAuth.getTimeWindowFromTime(tm);    int window = googleAuth.getConfig().getWindowSize();    for (int i = -((window - 1) / 2); i <= window / 2; ++i) {      String googleCode = googleAuth.calculateCode16(Base64.decodeBase64(googleAuthKey), timeWindow + i);      log.debug((timeWindow + i) + " googleCode: " + googleCode);      byte[] code = googleCode.getBytes(DEFAULT_CHARSET);      byte[] iv = new byte[16];      System.arraycopy(code, 0, iv, 0, code.length);      try {        String newKey = convertKey(aesKey, iv);        String decodedData = AES.decode(data, newKey, Base64.encodeBase64String(iv));        if (checkCallBack != null && !checkCallBack.isRight(decodedData)) {          continue;        }        if (originDataSign != null) {          String sign = DigestUtils.md5Hex(decodedData);          if (!sign.equalsIgnoreCase(originDataSign)) {            continue;          }        }        return decodedData;      } catch (DecodeException e) {        lastError = e;      }    }    if (lastError == null) {      lastError = new DecodeException("Decode Failed, Error Password!");    }    throw lastError;  }

signVerifyRequest

static boolean signVerifyRequest(Map<String, String[]> parameters, String rsaPublicKey, String sign, String security) throws SignVerifyException {    String preSignData = getHttpPreSignData(parameters, security);    log.debug("待验签字符串:" + preSignData);    return RSA.verify(preSignData.getBytes(DEFAULT_CHARSET), rsaPublicKey, sign);  } 

GoogleAuth

public class GoogleAuth {  private GoogleAuthenticatorConfig config;  private GoogleAuthenticator googleAuthenticator;  public GoogleAuth() {    GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder gacb =        new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder()            .setTimeStepSizeInMillis(TimeUnit.MINUTES.toMillis(2))            .setWindowSize(3)            .setCodeDigits(8)            .setKeyRepresentation(KeyRepresentation.BASE64);    config = gacb.build();    googleAuthenticator = new GoogleAuthenticator(config);  }  public GoogleAuthenticatorConfig getConfig(){    return config;  }  public void setConfig(GoogleAuthenticatorConfig c) {    config = c;    googleAuthenticator = new GoogleAuthenticator(config);  }  /**   * 认证   * @param encodedKey(Base 32/64)   * @param code   * @return 是否通过   */  public boolean authorize(String encodedKey, int code) {    return googleAuthenticator.authorize(encodedKey, code);  }  /**   * 生成 GoogleAuth Code   * @param keyBase64   * @return   */  public int getCodeValidCode(String keyBase64) {    int code = googleAuthenticator.getTotpPassword(keyBase64);    return code;  }  public long getTimeWindowFromTime(long time)  {    return time / this.config.getTimeStepSizeInMillis();  }  private static String formatLabel(String issuer, String accountName) {    if (accountName == null || accountName.trim().length() == 0) {      throw new IllegalArgumentException("Account name must not be empty.");    }    StringBuilder sb = new StringBuilder();    if (issuer != null) {      if (issuer.contains(":")) {        throw new IllegalArgumentException("Issuer cannot contain the /':/' character.");      }      sb.append(issuer);      sb.append(":");    }    sb.append(accountName);    return sb.toString();  }  public String getOtpAuthTotpURL(String keyBase64) throws EncoderException{    return getOtpAuthTotpURL("MLJR", "myname@mljr.com", keyBase64);  }  /**   * 生成GoogleAuth认证的URL,便于生成二维码   * @param issuer   * @param accountName   * @param keyBase32   * @return   */  public String getOtpAuthTotpURL(String issuer, String accountName, String keyBase32) throws EncoderException {    StringBuilder url = new StringBuilder();    url.append("otpauth://")        .append("totp")        .append("/").append(formatLabel(issuer, accountName));    Map<String, String> parameter = new HashMap<String, String>();    /**     * https://github.com/google/google-authenticator/wiki/Key-Uri-Format     * The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548.     */    parameter.put("secret", keyBase32);    if (issuer != null) {      if (issuer.contains(":")) {        throw new IllegalArgumentException("Issuer cannot contain the /':/' character.");      }      parameter.put("issuer", issuer);    }    parameter.put("algorithm", "SHA1");    parameter.put("digits", String.valueOf(config.getCodeDigits()));    parameter.put("period", String.valueOf(TimeUnit.MILLISECONDS.toSeconds(config.getTimeStepSizeInMillis())));    URLCodec urlCodec = new URLCodec();    if (!parameter.isEmpty()) {      url.append("?");      for(String key : parameter.keySet()) {        String value = parameter.get(key);        if (value == null){          continue;        }        value = urlCodec.encode(value);        url.append(key).append("=").append(value).append("&");      }    }    return url.toString();  }  private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";  private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";  private static final String HMAC_HASH_FUNCTION = "HmacSHA1";  private static final String HMAC_MD5_FUNCTION = "HmacMD5";  /**   * 基于时间 生成16位的 code   * @param key   * @param tm   * @return   */  public String calculateCode16(byte[] key, long tm)  {    // Allocating an array of bytes to represent the specified instant    // of time.    byte[] data = new byte[8];    long value = tm;    // Converting the instant of time from the long representation to a    // big-endian array of bytes (RFC4226, 5.2. Description).    for (int i = 8; i-- > 0; value >>>= 8)    {      data[i] = (byte) value;    }    // Building the secret key specification for the HmacSHA1 algorithm.    SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);    try    {      // Getting an HmacSHA1 algorithm implementation from the JCE.      Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);      // Initializing the MAC algorithm.      mac.init(signKey);      // Processing the instant of time and getting the encrypted data.      byte[] hash = mac.doFinal(data);      // Building the validation code performing dynamic truncation      // (RFC4226, 5.3. Generating an HOTP value)      int offset = hash[hash.length - 1] & 0xB;      // We are using a long because Java hasn't got an unsigned integer type      // and we need 32 unsigned bits).      long truncatedHash = 0;      for (int i = 0; i < 8; ++i)      {        truncatedHash <<= 8;        // Java bytes are signed but we need an unsigned integer:        // cleaning off all but the LSB.        truncatedHash |= (hash[offset + i] & 0xFF);      }      truncatedHash &= Long.MAX_VALUE;      truncatedHash %= 10000000000000000L;      // module with the maximum validation code value.      // Returning the validation code to the caller.      return String.format("%016d", truncatedHash);    } catch (InvalidKeyException e) {      throw new GoogleAuthenticatorException("The operation cannot be "          + "performed now.");    } catch (NoSuchAlgorithmException ex) {      // We're not disclosing internal error details to our clients.      throw new GoogleAuthenticatorException("The operation cannot be "          + "performed now.");    }  }}

GoogleAuth其他代码 看这里

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表