前言
从去年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, 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其他代码 看这里
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。