经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringMvc/SpringBoot HTTP通信加解密的实现
来源:jb51  时间:2019/8/15 12:22:59  对本文有异议

前言

从去年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

  1. @Controller
  2. @RequestMapping("/api/demo")
  3. public class MyDemoController {
  4.  
  5. @RequestDecode
  6. @ResponseBody
  7. @RequestMapping(value = "user", method = RequestMethod.POST)
  8. public ResponseDto addUser(
  9. @RequestBody User user
  10. ) throws Exception {
  11. //TODO ...
  12. }
  13.  
  14. }
  15.  
  1. /**
  2. * 解密请求数据
  3. */
  4. @Target(ElementType.METHOD)
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Documented
  7. public @interface RequestDecode {
  8.  
  9. SecurityMethod method() default SecurityMethod.NULL;
  10.  
  11. }

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

2. 建设自己的RequestBodyAdvice

有了上面的入口定义,接下来处理解密这件事,目的很明确:

1. 是否需要解密判断httpHeader中的encodeMethod字段。

2. 在进入controller之前就解密完成,是controller处理逻辑无感知。

DecodeRequestBodyAdvice.java

  1. @Slf4j
  2. @Component
  3. @ControllerAdvice(basePackages = "com.xxx.hr.api.controller")
  4. public class DecodeRequestBodyAdvice implements RequestBodyAdvice {
  5.  
  6. @Value("${hrapi.aesKey}")
  7. String aesKey;
  8. @Value("${hrapi.googleKey}")
  9. String googleKey;
  10.  
  11. @Override
  12. public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  13. return methodParameter.getMethodAnnotation(RequestDecode.class) != null
  14. && methodParameter.getParameterAnnotation(RequestBody.class) != null;
  15. }
  16.  
  17. @Override
  18. public Object handleEmptyBody(Object body, HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  19. return body;
  20. }
  21.  
  22. @Override
  23. public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
  24. RequestDecode requestDecode = parameter.getMethodAnnotation(RequestDecode.class);
  25. if (requestDecode == null) {
  26. return request;//controller方法不要求加解密
  27. }
  28. String appId = request.getHeaders().getFirst(com.xxx.hr.bean.constant.HttpHeaders.APP_ID);//这里是扩展,可以知道来源方(如开放平台使用)
  29.  
  30. String encodeMethod = request.getHeaders().getFirst(com.xxx.hr.bean.constant.HttpHeaders.ENCODE_METHOD);
  31. if (StringUtils.isEmpty(encodeMethod)) {
  32. return request;
  33. }
  34. SecurityMethod encodeMethodEnum = SecurityMethod.getByCode(encodeMethod);
  35. //这里灵活的可以支持到多种加解密方式
  36. switch (encodeMethodEnum) {
  37. case NULL:
  38. break;
  39. case AES: {
  40. InputStream is = request.getBody();
  41. ByteBuf buf = PooledByteBufAllocator.DEFAULT.heapBuffer();
  42. int ret = -1;
  43. int len = 0;
  44. while((ret = is.read()) > 0) {
  45. buf.writeByte(ret);
  46. len ++;
  47. }
  48. String body = buf.toString(0, len, xxxSecurity.DEFAULT_CHARSET);
  49. buf.release();
  50. String temp = null;
  51. try {
  52. temp = XxxSecurity.aesDecodeData(body, aesKey, googleKey, new CheckCallBack() {
  53. @Override
  54. public boolean isRight(String data) {
  55. return data != null && (data.startsWith("{") || data.startsWith("["));
  56. }
  57. });
  58. log.info("解密完成: {}", temp);
  59. return new DecodedHttpInputMessage(request.getHeaders(), new ByteArrayInputStream(temp.getBytes("UTF-8")));
  60. } catch (DecodeException e) {
  61. log.warn("解密失败 appId: {}, Name:{} 待解密密文: {}", appId, partnerName, body, e);
  62. throw e;
  63. }
  64. }
  65. }
  66. return request;
  67. }
  68.  
  69. @Override
  70. public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  71. return body;
  72. }
  73.  
  74. static class DecodedHttpInputMessage implements HttpInputMessage {
  75. HttpHeaders headers;
  76. InputStream body;
  77.  
  78. public DecodedHttpInputMessage(HttpHeaders headers, InputStream body) {
  79. this.headers = headers;
  80. this.body = body;
  81. }
  82.  
  83. @Override
  84. public InputStream getBody() throws IOException {
  85. return body;
  86. }
  87.  
  88. @Override
  89. public HttpHeaders getHeaders() {
  90. return headers;
  91. }
  92. }
  93. }

至此加解密完成了。

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

响应加密

下面附件一下响应加密过程,目的

1. Controller逻辑代码无感知
2. 可以一键开关响应加密

定义Controller

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

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

建设自己的ResponseBodyAdvice

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

  1. @Slf4j
  2. @Component
  3. @ControllerAdvice(basePackages = "com.xxx.hr.api.controller")
  4. public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {
  5.  
  6. @Autowired
  7. PartnerService partnerService;
  8.  
  9. @Override
  10. public boolean supports(MethodParameter returnType, Class converterType) {
  11. return returnType.getMethodAnnotation(ResponseEncode.class) != null;
  12. }
  13.  
  14. @Override
  15. public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
  16. ResponseEncode responseEncode = returnType.getMethodAnnotation(ResponseEncode.class);
  17. String uid = request.getHeaders().getFirst(HttpHeaders.PARTNER_UID);
  18. if (uid == null) {
  19. uid = request.getHeaders().getFirst(HttpHeaders.APP_ID);
  20. }
  21. PartnerConfig config = partnerService.getConfigByAppId(uid);
  22. if (responseEncode.method() == SecurityMethod.NULL || responseEncode.method() == SecurityMethod.AES) {
  23. if (config == null) {
  24. return ResponseDto.rsFail(ResponseCode.E_403, "商户不存在");
  25. }
  26. String temp = JSON.toJSONString(body);
  27. log.debug("待加密数据: {}", temp);
  28. String encodedBody = XxxSecurity.aesEncodeData(temp, config.getEncryptionKey(), config.getGoogleKey());
  29. log.debug("加密完成: {}", encodedBody);
  30. response.getHeaders().set(HttpHeaders.ENCODE_METHOD, HttpHeaders.VALUE.AES);
  31. response.getHeaders().set(HttpHeaders.HEADER_CONTENT_TYPE, HttpHeaders.VALUE.APPLICATION_BASE64_JSON_UTF8);
  32. response.getHeaders().remove(HttpHeaders.SIGN_METHOD);
  33. return encodedBody;
  34. }
  35. return body;
  36.  
  37. }
  38.  
  39. }

拓展

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

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

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

  1. @RequestSign
  2. @ResponseEncode
  3. @ResponseBody
  4. @RequestMapping(value = "employee", method = RequestMethod.GET)
  5. public ResponseDto<UserEEInfo> userEEInfo(
  6. @RequestParam(HttpHeaders.UID) String uid
  7. ) {
  8. //TODO ...
  9. }

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

  1. @Slf4j
  2. @Component
  3. public class SignInterceptor implements HandlerInterceptor {
  4.  
  5. @Autowired
  6. PartnerService partnerService;
  7.  
  8. @Override
  9. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  10. HandlerMethod method = (HandlerMethod) handler;
  11. RequestSign requestSign = method.getMethodAnnotation(RequestSign.class);
  12. if (requestSign == null) {
  13. return true;
  14. }
  15.  
  16. String appId = request.getHeader(HttpHeaders.APP_ID);
  17. ValidateUtils.notTrimEmptyParam(appId, "Header[appId]");
  18.  
  19. PartnerConfig config = partnerService.getConfigByAppId(appId);
  20. ValidateUtils.notNull(config, Code.E_400, "商戶不存在");
  21. String partnerName = partnerService.getPartnerName(appId);
  22.  
  23. String sign = request.getParameter(HttpHeaders.SIGN);
  24. String signMethod = request.getParameter(HttpHeaders.SIGN_METHOD);
  25. signMethod = (signMethod == null) ? "RSA" : signMethod;
  26. Map<String, String[]> parameters = request.getParameterMap();
  27. ValidateUtils.notTrimEmptyParam(sign, "sign");
  28. if ("RSA".equals(signMethod)) {
  29. sign = sign.replaceAll(" ", "+");
  30. boolean isOK = xxxxSecurity.signVerifyRequest(parameters, config.getRsaPublicKey(), sign, config.getSecurity());
  31. if (isOK) {
  32. log.info("验证商户签名通过 {}[{}] ", appId, partnerName);
  33. return true;
  34. } else {
  35. log.warn("验证商户签名失败 {}[{}] ", appId, partnerName);
  36. }
  37. } else {
  38. throw new SignVerifyException("暂不支持该签名");
  39. }
  40. throw new SignVerifyException("签名校验失败");
  41. }
  42.  
  43. @Override
  44. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  45. }
  46.  
  47. @Override
  48. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  49.  
  50. }
  51. }

各个枚举定义:

  1. //加解密、签名算法枚举
  2. public enum SecurityMethod {
  3.  
  4. NULL,
  5.  
  6. AES,
  7. RSA,
  8. DES,
  9. DES3,
  10.  
  11. SHA1,
  12. MD5
  13. ;
  14.  
  15. }

注解定义:

  1. /**
  2. * 请求数据数据需要解密
  3. */
  4. @Target(ElementType.METHOD)
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Documented
  7. public @interface RequestDecode {
  8.  
  9. SecurityMethod method() default SecurityMethod.NULL;
  10.  
  11. }
  12.  
  13. /**
  14. * 请求数据需要验签
  15. */
  16. @Target({ElementType.TYPE, ElementType.METHOD})
  17. @Retention(RetentionPolicy.RUNTIME)
  18. @Documented
  19. @Inherited
  20. public @interface RequestSign {
  21.  
  22. SecurityMethod method() default SecurityMethod.RSA;
  23.  
  24. }
  25.  
  26. /**
  27. * 数据响应需要加密
  28. */
  29. @Target(ElementType.METHOD)
  30. @Retention(RetentionPolicy.RUNTIME)
  31. @Documented
  32. public @interface ResponseEncode {
  33.  
  34. SecurityMethod method() default SecurityMethod.NULL;
  35.  
  36. }
  37.  
  38. /**
  39. * 响应数据需要生成签名
  40. */
  41. @Target(ElementType.METHOD)
  42. @Retention(RetentionPolicy.RUNTIME)
  43. @Documented
  44. @Inherited
  45. public @interface ResponseSign {
  46.  
  47. SecurityMethod method() default SecurityMethod.NULL;
  48.  
  49. }

aesDecodeData

  1. /**
  2. * AES 解密数据
  3. *
  4. * @param data 待解密数据
  5. * @param aesKey AES 密钥(BASE64)
  6. * @param googleAuthKey GoogleAuthKey(BASE64)
  7. * @param originDataSign 原始数据md5签名
  8. * @return
  9. */
  10. public static String aesDecodeDataEx(String data, String aesKey, String googleAuthKey, String originDataSign) {
  11. return aesDecodeData(data, aesKey, googleAuthKey, System.currentTimeMillis(), null, originDataSign);
  12. }
  13.  
  14. public static String aesDecodeData(String data, String aesKey, String googleAuthKey, long tm, CheckCallBack checkCallBack, String originDataSign) {
  15. DecodeException lastError = null;
  16. long timeWindow = googleAuth.getTimeWindowFromTime(tm);
  17. int window = googleAuth.getConfig().getWindowSize();
  18. for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
  19. String googleCode = googleAuth.calculateCode16(Base64.decodeBase64(googleAuthKey), timeWindow + i);
  20. log.debug((timeWindow + i) + " googleCode: " + googleCode);
  21. byte[] code = googleCode.getBytes(DEFAULT_CHARSET);
  22. byte[] iv = new byte[16];
  23. System.arraycopy(code, 0, iv, 0, code.length);
  24. try {
  25. String newKey = convertKey(aesKey, iv);
  26. String decodedData = AES.decode(data, newKey, Base64.encodeBase64String(iv));
  27. if (checkCallBack != null && !checkCallBack.isRight(decodedData)) {
  28. continue;
  29. }
  30. if (originDataSign != null) {
  31. String sign = DigestUtils.md5Hex(decodedData);
  32. if (!sign.equalsIgnoreCase(originDataSign)) {
  33. continue;
  34. }
  35. }
  36. return decodedData;
  37. } catch (DecodeException e) {
  38. lastError = e;
  39. }
  40. }
  41. if (lastError == null) {
  42. lastError = new DecodeException("Decode Failed, Error Password!");
  43. }
  44. throw lastError;
  45. }

signVerifyRequest

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

GoogleAuth

  1. public class GoogleAuth {
  2.  
  3. private GoogleAuthenticatorConfig config;
  4. private GoogleAuthenticator googleAuthenticator;
  5.  
  6. public GoogleAuth() {
  7. GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder gacb =
  8. new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder()
  9. .setTimeStepSizeInMillis(TimeUnit.MINUTES.toMillis(2))
  10. .setWindowSize(3)
  11. .setCodeDigits(8)
  12. .setKeyRepresentation(KeyRepresentation.BASE64);
  13.  
  14. config = gacb.build();
  15. googleAuthenticator = new GoogleAuthenticator(config);
  16. }
  17.  
  18. public GoogleAuthenticatorConfig getConfig(){
  19. return config;
  20. }
  21.  
  22. public void setConfig(GoogleAuthenticatorConfig c) {
  23. config = c;
  24. googleAuthenticator = new GoogleAuthenticator(config);
  25. }
  26.  
  27.  
  28. /**
  29. * 认证
  30. * @param encodedKey(Base 32/64)
  31. * @param code
  32. * @return 是否通过
  33. */
  34. public boolean authorize(String encodedKey, int code) {
  35. return googleAuthenticator.authorize(encodedKey, code);
  36. }
  37.  
  38. /**
  39. * 生成 GoogleAuth Code
  40. * @param keyBase64
  41. * @return
  42. */
  43. public int getCodeValidCode(String keyBase64) {
  44. int code = googleAuthenticator.getTotpPassword(keyBase64);
  45. return code;
  46. }
  47.  
  48. public long getTimeWindowFromTime(long time)
  49. {
  50. return time / this.config.getTimeStepSizeInMillis();
  51. }
  52.  
  53. private static String formatLabel(String issuer, String accountName) {
  54. if (accountName == null || accountName.trim().length() == 0) {
  55. throw new IllegalArgumentException("Account name must not be empty.");
  56. }
  57. StringBuilder sb = new StringBuilder();
  58. if (issuer != null) {
  59. if (issuer.contains(":")) {
  60. throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
  61. }
  62. sb.append(issuer);
  63. sb.append(":");
  64. }
  65. sb.append(accountName);
  66. return sb.toString();
  67. }
  68.  
  69. public String getOtpAuthTotpURL(String keyBase64) throws EncoderException{
  70. return getOtpAuthTotpURL("MLJR", "myname@mljr.com", keyBase64);
  71. }
  72.  
  73. /**
  74. * 生成GoogleAuth认证的URL,便于生成二维码
  75. * @param issuer
  76. * @param accountName
  77. * @param keyBase32
  78. * @return
  79. */
  80. public String getOtpAuthTotpURL(String issuer, String accountName, String keyBase32) throws EncoderException {
  81. StringBuilder url = new StringBuilder();
  82. url.append("otpauth://")
  83. .append("totp")
  84. .append("/").append(formatLabel(issuer, accountName));
  85. Map<String, String> parameter = new HashMap<String, String>();
  86. /**
  87. * https://github.com/google/google-authenticator/wiki/Key-Uri-Format
  88. * The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548.
  89. */
  90. parameter.put("secret", keyBase32);
  91. if (issuer != null) {
  92. if (issuer.contains(":")) {
  93. throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
  94. }
  95. parameter.put("issuer", issuer);
  96. }
  97. parameter.put("algorithm", "SHA1");
  98. parameter.put("digits", String.valueOf(config.getCodeDigits()));
  99. parameter.put("period", String.valueOf(TimeUnit.MILLISECONDS.toSeconds(config.getTimeStepSizeInMillis())));
  100. URLCodec urlCodec = new URLCodec();
  101. if (!parameter.isEmpty()) {
  102. url.append("?");
  103. for(String key : parameter.keySet()) {
  104. String value = parameter.get(key);
  105. if (value == null){
  106. continue;
  107. }
  108. value = urlCodec.encode(value);
  109. url.append(key).append("=").append(value).append("&");
  110. }
  111. }
  112. return url.toString();
  113.  
  114. }
  115.  
  116. private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
  117. private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
  118. private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
  119. private static final String HMAC_MD5_FUNCTION = "HmacMD5";
  120.  
  121. /**
  122. * 基于时间 生成16位的 code
  123. * @param key
  124. * @param tm
  125. * @return
  126. */
  127. public String calculateCode16(byte[] key, long tm)
  128. {
  129. // Allocating an array of bytes to represent the specified instant
  130. // of time.
  131. byte[] data = new byte[8];
  132. long value = tm;
  133.  
  134. // Converting the instant of time from the long representation to a
  135. // big-endian array of bytes (RFC4226, 5.2. Description).
  136. for (int i = 8; i-- > 0; value >>>= 8)
  137. {
  138. data[i] = (byte) value;
  139. }
  140.  
  141. // Building the secret key specification for the HmacSHA1 algorithm.
  142. SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
  143. try
  144. {
  145. // Getting an HmacSHA1 algorithm implementation from the JCE.
  146. Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
  147. // Initializing the MAC algorithm.
  148. mac.init(signKey);
  149. // Processing the instant of time and getting the encrypted data.
  150. byte[] hash = mac.doFinal(data);
  151. // Building the validation code performing dynamic truncation
  152. // (RFC4226, 5.3. Generating an HOTP value)
  153. int offset = hash[hash.length - 1] & 0xB;
  154. // We are using a long because Java hasn't got an unsigned integer type
  155. // and we need 32 unsigned bits).
  156. long truncatedHash = 0;
  157. for (int i = 0; i < 8; ++i)
  158. {
  159. truncatedHash <<= 8;
  160. // Java bytes are signed but we need an unsigned integer:
  161. // cleaning off all but the LSB.
  162. truncatedHash |= (hash[offset + i] & 0xFF);
  163. }
  164.  
  165. truncatedHash &= Long.MAX_VALUE;
  166. truncatedHash %= 10000000000000000L;
  167. // module with the maximum validation code value.
  168. // Returning the validation code to the caller.
  169. return String.format("%016d", truncatedHash);
  170. } catch (InvalidKeyException e) {
  171. throw new GoogleAuthenticatorException("The operation cannot be "
  172. + "performed now.");
  173. } catch (NoSuchAlgorithmException ex) {
  174. // We're not disclosing internal error details to our clients.
  175. throw new GoogleAuthenticatorException("The operation cannot be "
  176. + "performed now.");
  177. }
  178. }
  179. }

GoogleAuth其他代码 看这里

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号