您好,登錄后才能下訂單哦!
這篇文章主要介紹SpringMvc/SpringBoot如何實現HTTP通信加解密,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
近來很多人問到下面的問題
我們不想在每個Controller方法收到字符串報文后再調用一次解密,雖然可以完成,但是很low,且如果想不再使用加解密,修改起來很是麻煩。
我們想在使用Rest工具或swagger請求的時候不進行加解密,而在app調用的時候處理加解密,這可如何操作。
針對以上的問題,下面直接給出解決方案:
實現思路
APP調用API的時候,如果需要加解密的接口,需要在httpHeader中給出加密方式,如header[encodeMethod]。
Rest工具或swagger請求的時候無需指定此header。
后端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通信加解密”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。