背景 :
第三方客户端访问系统接口时,需要对第三方客户端传递参数进行加密传输,同时在服务端网关进行解密,保证参数路由到其它内部服务时参数状态为解密状态.
流程:
第三方客户端→网关解密→路由到指定微服务接口
将需要加密传输的请求参数做成jar包或依赖包提供给第三方 , 并在服务端新增解密逻辑判断需要解密的请求进行解密处理.
@Override
public ResponseEntity
package *;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public final class RSAUtil {
/**
* 加密类型
*/
private static final String RSA_ALGORITHM = "RSA";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* 公钥加密
*
* @param dataBase64String 需要加密数组
* @param publicKeyString 公钥
* @return 加密后数组
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws InvalidKeyException
*/
public static final String encryptByPublicKey(String dataBase64String, String publicKeyString) throws NoSuchAlgorithmException,
InvalidKeySpecException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException, IOException {
byte[] data = dataBase64String.getBytes("UTF-8");
byte[] keyBytes = Base64.getDecoder().decode(publicKeyString);
//实例化密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
//初始化公钥,根据给定的编码密钥创建一个新的 X509EncodedKeySpec。
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
//数据加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return Base64.getEncoder().encodeToString(encryptedData);
}
/**
* 公钥解密
*
* @param encryptedData 待解密数据
* @param publicKeyString 公钥
* @return byte[] 解密数据
*/
public static final byte[] decryptByPublicKey(byte[] encryptedData, String publicKeyString) throws Exception {
byte[] key = Base64.getDecoder().decode(publicKeyString);
//实例化密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
//初始化公钥
//密钥材料转换
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
//产生公钥
PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
//数据解密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, pubKey);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
}
1.调用HttpUtil传递参数
这里使用spring自带的 restTemplate
使用默认的 httpclient 进行调用,也可以考虑情况集成开源的okhttp3进行使用.
public final static ResponseEntity<Map> postForObject(String url, String params) {
String[] requestParams = params.split("-");
//设置Http的Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
//设置固定请求头
headers.add("apiAuthAccessEnable", "true");
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("data", requestParams[0]);
paramMap.add("secretId", requestParams[1]);
//设置访问的Entity
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(paramMap, headers);
ResponseEntity<Map> responseEntity = null;
try {
responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Map.class);
} catch (RestClientException e) {
if ("401".contains(e.getMessage().trim())) {
HashMap<String, Object> tempBody = new HashMap<>();
tempBody.put("status", HttpStatus.UNAUTHORIZED.value());
tempBody.put("message", "Unauthorized Access");
ResponseEntity<Map> temp = new ResponseEntity<Map>(tempBody, HttpStatus.UNAUTHORIZED);
responseEntity = temp;
}
}
return responseEntity;
}
HttpServletRequestWrapper
解密请求参数RSA私钥解密请求参数 ,通过继承 HttpServletRequestWrapper
对请求完成解密过程 , 同时开放解密参数提供给 Filter
使用
HttpServletRequestWrapper
相关代码package *;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import *.entity.ApiAuthSecretPO;
import *.service.ApiAuthSecretService;
import *.utils.RSAUtil;
import *.utils.SpringContextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
public final class ApiAuthRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = LoggerFactory.getLogger(ApiAuthRequestWrapper.class);
private byte[] body;
private byte[] requestParam = null;
public ApiAuthRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
inputStream2String(request.getInputStream());
}
@Override
public final ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestParam);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
/**
* 将 inputStream 里的数据读取出来并转换成字符串
*
* @param inputStream inputStream
* @return String
*/
public final byte[] inputStream2String(InputStream inputStream) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
sb.append("get body params fail");
logger.error(e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
String requestParam = "";
if (sb.toString().trim().length() == 0) {
return new byte[]{};
}
JSONObject jsonObject = JSONObject.parseObject(sb.toString());
String decryptResult = "";
try {
String dataString = jsonObject.getString("data")
.replace("[", "").replace("]", "");
String dataSubstring = dataString.substring(1, dataString.length() - 1);
String secretIdStr = jsonObject.getString("secretId")
.replace("[", "").replace("]", "");
String secretIdSubstring = secretIdStr.substring(1, secretIdStr.length() - 1);
ApiAuthSecretService apiAuthSecretService = SpringContextUtil.getBean(ApiAuthSecretService.class);
ApiAuthSecretPO apiAuthSecretPO = apiAuthSecretService.getByApiInterfaceAuthId(secretIdSubstring);
decryptResult = RSAUtil.decryptByPrivateKey(dataSubstring, apiAuthSecretPO.getPrivateKey());
requestParam = decryptResult;
} catch (Exception e) {
logger.error("参数解密失败,原因:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
this.body = decryptResult.getBytes();
JSONObject temp = JSON.parseObject(requestParam);
this.requestParam = JSON.toJSONString(temp.get("data")).getBytes();
return this.requestParam;
}
public final String getRequestBody() throws UnsupportedEncodingException {
String body = new String(this.body, "UTF-8");
return body;
}
public final String getRequestParam() throws UnsupportedEncodingException {
String param = new String(this.requestParam, "UTF-8");
return param;
}
}
Filter
对相应参数进行操作将解密后请求参数从 HttpServletRequestWrapper
取出 , 并在过滤器中进行业务验证及其它操作 , 对符合拦截条件的请求进行拦截处理, 对不符合拦截请求的参数进行放行处理(及不需要解密逻辑).
package *.configure;
import com.alibaba.fastjson.JSONObject;
import *.entity.ApiAuthPO;
import *.service.ApiAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Component
@WebFilter(filterName = "xxxFilter", urlPatterns = {"/xxx/*"})
@ConditionalOnExpression("${xxx.enable:true}")
public class ApiAuthenticationFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(ApiAuthenticationFilter.class);
public static final String API_AUTH_ACCESS_KEY = "apiAuthAccessEnable";
@Autowired
private ApiAuthService apiAuthService;
@Autowired
private StringRedisTemplate redisTemplate;
public final String redisKey = "xxx_key:";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String pathTotal = request.getRequestURI();
String pathInterface = pathTotal.substring(pathTotal.indexOf("/", 2), pathTotal.length());
if (request.getHeader("User-Agent").startsWith("Mozilla")) {
chain.doFilter(request, response);
} else {
Boolean apiAuthFlag = Boolean.valueOf(request.getHeader(API_AUTH_ACCESS_KEY));
if (!apiAuthFlag) {
chain.doFilter(request, response);
return;
}
ApiAuthRequestWrapper apiAuthRequestWrapper = null;
try {
// 创建重写后的 HttpServletRequest
apiAuthRequestWrapper = new ApiAuthRequestWrapper((HttpServletRequest) servletRequest);
String requestParamStr = apiAuthRequestWrapper.getRequestBody();
JSONObject requestParamObject = JSONObject.parseObject(requestParamStr);
Long timestamp = Long.valueOf((String) requestParamObject.get("timestamp"));
String project = (String) requestParamObject.get("project");
String appId = (String) requestParamObject.get("appId");
ApiAuthPO apiAuthPO = apiAuthService.getById(appId);
boolean repeatedBoolean = validateRepeatedRequest(timestamp, appId);
boolean matchBoolean = Stream.of(apiAuthPO)
.filter(s -> s.getProject().equals(project))
.filter(s -> pathInterface.equals(s.getApiPath()))
.filter(s -> new Date().before(s.getInvalidTime()))
.anyMatch(s -> 1 == s.getIsEnable());
if (repeatedBoolean || !matchBoolean) {
changeUnauthorizedInfo(response, pathTotal);
return;
}
} catch (Exception e) {
logger.error("参数解密失败,原因:{}", e.getMessage());
changeUnauthorizedInfo(response, pathTotal);
return;
}
chain.doFilter(apiAuthRequestWrapper, response);
}
}
/**
* 校验重复请求
*
* @param timestamp 请求时间戳
* @param appId 请求应用id
* @return 重复请求返回 true 否则返回false
*/
private Boolean validateRepeatedRequest(Long timestamp, String appId) {
Boolean repeatedBoolean = false;
if (System.currentTimeMillis() - timestamp > 1000 * 60 * 5 || System.currentTimeMillis() - timestamp < 0) {
repeatedBoolean = true;
} else {
String key = redisKey + appId + ":";
repeatedBoolean = !redisTemplate.opsForValue().setIfAbsent(key + timestamp, String.valueOf(timestamp), 5, TimeUnit.MINUTES);
}
return repeatedBoolean;
}
/**
* 返回未授权信息
*
* @param response
* @param path
* @throws IOException
*/
private void changeUnauthorizedInfo(HttpServletResponse response, String path) throws IOException {
logger.debug("当前接口未获取到授权信息!,接口路径为:{}", path);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Unauthorized Access!");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}