這篇文章主要介紹SpringMvc/SpringBoot如何實(shí)現(xiàn)HTTP通信加解密,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!
創(chuàng)新互聯(lián)主要從事成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、成都外貿(mào)網(wǎng)站建設(shè)公司、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)沂南,十載網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):18980820575
近來很多人問到下面的問題
我們不想在每個(gè)Controller方法收到字符串報(bào)文后再調(diào)用一次解密,雖然可以完成,但是很low,且如果想不再使用加解密,修改起來很是麻煩。
我們想在使用Rest工具或swagger請(qǐng)求的時(shí)候不進(jìn)行加解密,而在app調(diào)用的時(shí)候處理加解密,這可如何操作。
針對(duì)以上的問題,下面直接給出解決方案:
實(shí)現(xiàn)思路
APP調(diào)用API的時(shí)候,如果需要加解密的接口,需要在httpHeader中給出加密方式,如header[encodeMethod]。
Rest工具或swagger請(qǐng)求的時(shí)候無需指定此header。
后端API收到request后,判斷header中的encodeMethod字段,如果有值,則認(rèn)為是需要解密,否則就認(rèn)為是明文。
約定
為了精簡分享技術(shù),先約定只處理POST上傳JSON(application/json)數(shù)據(jù)的加解密處理。
請(qǐng)求解密實(shí)現(xiàn)方式
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 ... } }
/** * 解密請(qǐng)求數(shù)據(jù) */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestDecode { SecurityMethod method() default SecurityMethod.NULL; }
可以看到這里的Controller定義的很普通,只有一個(gè)額外的自定義注解RequestDecode,這個(gè)注解是為了下面的RequestBodyAdvice的使用。
2. 建設(shè)自己的RequestBodyAdvice
有了上面的入口定義,接下來處理解密這件事,目的很明確:
1. 是否需要解密判斷httpHeader中的encodeMethod字段。
2. 在進(jìn)入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);//這里是擴(kuò)展,可以知道來源方(如開放平臺(tái)使用) 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; } } }
至此加解密完成了。
————————-華麗分割線 —————————–
響應(yīng)加密
下面附件一下響應(yīng)加密過程,目的
1. Controller邏輯代碼無感知
2. 可以一鍵開關(guān)響應(yīng)加密
定義Controller
@ResponseEncode @ResponseBody @RequestMapping(value = "employee", method = RequestMethod.GET) public ResponseDtouserEEInfo( @ApiParam("用戶編號(hào)") @RequestParam(HttpHeaders.APPID) Long userId ) { //TODO ... }
/** * 加密響應(yīng)數(shù)據(jù) */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseEncode { SecurityMethod method() default SecurityMethod.NULL; }
這里的Controller定義的也很普通,只有一個(gè)額外的自定義注解ResponseEncode,這個(gè)注解是為了下面的ResponseBodyAdvice的使用。
建設(shè)自己的ResponseBodyAdvice
這里約定將響應(yīng)的DTO序列化為JSON格式數(shù)據(jù),然后再加密,最后在響應(yīng)給請(qǐng)求方。
@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("待加密數(shù)據(jù): {}", 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; } }
拓展
由上面的實(shí)現(xiàn),如何實(shí)現(xiàn)RSA驗(yàn)證簽名呢?這個(gè)就簡單了,請(qǐng)看分解。
目的還是很簡單,進(jìn)來減少對(duì)業(yè)務(wù)邏輯的入侵。
首先設(shè)定一下那些請(qǐng)求需要驗(yàn)證簽名
@RequestSign @ResponseEncode @ResponseBody @RequestMapping(value = "employee", method = RequestMethod.GET) public ResponseDtouserEEInfo( @RequestParam(HttpHeaders.UID) String uid ) { //TODO ... }
這里還是使用一個(gè)注解RequestSign,然后再實(shí)現(xiàn)一個(gè)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; Mapparameters = 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("驗(yàn)證商戶簽名通過 {}[{}] ", appId, partnerName); return true; } else { log.warn("驗(yàn)證商戶簽名失敗 {}[{}] ", appId, partnerName); } } else { throw new SignVerifyException("暫不支持該簽名"); } throw new SignVerifyException("簽名校驗(yàn)失敗"); } @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 { } }
各個(gè)枚舉定義:
//加解密、簽名算法枚舉 public enum SecurityMethod { NULL, AES, RSA, DES, DES3, SHA1, MD5 ; }
注解定義:
/** * 請(qǐng)求數(shù)據(jù)數(shù)據(jù)需要解密 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestDecode { SecurityMethod method() default SecurityMethod.NULL; } /** * 請(qǐng)求數(shù)據(jù)需要驗(yàn)簽 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestSign { SecurityMethod method() default SecurityMethod.RSA; } /** * 數(shù)據(jù)響應(yīng)需要加密 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseEncode { SecurityMethod method() default SecurityMethod.NULL; } /** * 響應(yīng)數(shù)據(jù)需要生成簽名 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface ResponseSign { SecurityMethod method() default SecurityMethod.NULL; }
aesDecodeData
/** * AES 解密數(shù)據(jù) * * @param data 待解密數(shù)據(jù) * @param aesKey AES 密鑰(BASE64) * @param googleAuthKey GoogleAuthKey(BASE64) * @param originDataSign 原始數(shù)據(jù)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(Mapparameters, String rsaPublicKey, String sign, String security) throws SignVerifyException { String preSignData = getHttpPreSignData(parameters, security); log.debug("待驗(yàn)簽字符串:" + 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); } /** * 認(rèn)證 * @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認(rèn)證的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)); Mapparameter = new HashMap (); /** * 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"; /** * 基于時(shí)間 生成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如何實(shí)現(xiàn)HTTP通信加解密”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!