温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

SpringMvc/SpringBoot如何实现HTTP通信加解密

发布时间:2021-07-08 11:45:13 来源:亿速云 阅读:259 作者:小新 栏目:编程语言

这篇文章主要介绍SpringMvc/SpringBoot如何实现HTTP通信加解密,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

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

  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) @Documented public @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) @Documented public @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 @Component public 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) @Documented public @interface RequestDecode {   SecurityMethod method() default SecurityMethod.NULL; } /**  * 请求数据需要验签  */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestSign {   SecurityMethod method() default SecurityMethod.RSA; } /**  * 数据响应需要加密  */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseEncode {   SecurityMethod method() default SecurityMethod.NULL; } /**  * 响应数据需要生成签名  */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @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.");     }   } }

以上是“SpringMvc/SpringBoot如何实现HTTP通信加解密”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注亿速云行业资讯频道!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI