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

网友投稿 448 2023-07-27

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

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

前言

从去年10月份到现在忙的没时间写博客了,今天就甩给大家一个干货吧!!!

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

我们不想在每个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, yfGqaCClass 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(

@ApiParam("用户编号") @RequestParam(HttpHeaders.APPID) Long userId

) {

//TODO ...

}

/**

* 加密响应数据

*/

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface ResponseEncode {

SecurityMethod method() default SecurityMethod.NULL;

}

这里的Controller定义的也很普通,只有一个额外的自定义注解ResponseEncode,这http://个注解是为了下面的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(

@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 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 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 parameter = 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";

/**

* 基于时间 生成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其他代码 看这里

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:IDEA创建parent项目(聚合项目)
下一篇:javaWeb使用验证码实现简单登录
相关文章

 发表评论

暂时没有评论,来抢沙发吧~