SpringMvc/SpringBoot HTTP通信加解密

8 篇文章 0 订阅
2 篇文章 0 订阅

前言

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

@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.");
        }
    }


}

GoogleAuth其他代码 看 这里

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值