在項目開發中,很多場景下我們的接口參數都需要進行加解密處理。
例如我開發的此項目中,參數原文使用AES加密為 params
字段,AES key使用RSA加密為 encKey
字段。
不統一處理
如果不對參數解密進行統一處理
@PostMapping("login")
public ResponseEntity login(HttpServletRequest request){
String params = request.getParameter("params");
String encKey = request.getParameter("encKey");
/* 1.RSA私鑰解密出AES的KEY */
PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey), privateKey);
/* 2.AES解密出原始參數 */
String decrypt = AESUtil.decrypt(params, new String(aesKey));
JSONObject originParam = JSONObject.parseObject(decrypt);
}
則需在每個需參數解密的方法進行參數解密,代碼冗余度較高
方案1:HttpServletRequestWrapper
- 定義一個
HttpServletRequestWrapper
,并在Wrapper中實現參數解密邏輯
@Slf4j
public class EncHttpServletRequest extends HttpServletRequestWrapper {
private JSONObject originParam;
private String encKey;
private String params;
public EncHttpServletRequest(HttpServletRequest request) throws GlobalException{
super(request);
String encKey = request.getParameter("encKey");
String params = request.getParameter("params");
this.encKey = encKey;
this.params = params;
if(StringUtils.isEmpty(encKey)||StringUtils.isEmpty(params)){
throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM);
}
byte[] aesKey;
try{
/* 1.RSA私鑰解密出AES的KEY */
PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey), privateKey);
/* 2.AES解密出原始參數 */
String decrypt = AESUtil.decrypt(params, new String(aesKey));
originParam = JSONObject.parseObject(decrypt);
}catch (Exception e){
e.printStackTrace();
throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM_DECRYPT);
}
}
}
- 再定義一個Filter,對請求進行Wrap
@Slf4j
@Component
@Order
@WebFilter(urlPatterns = "/*", filterName = "paramsDecryptFilter")
public class ParamsDecryptFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)servletRequest;
HttpServletResponse rsp = (HttpServletResponse)servletResponse;
// 對需要wrap的請求進行判斷轉換
if(HttpMethod.POST.name().equals(req.getMethod())){
EncHttpServletRequest encHttpServletRequest = null;
try{
encHttpServletRequest = new EncHttpServletRequest(req);
}catch (GlobalException e){
rsp.setContentType("application/json; charset=utf-8");
try (PrintWriter writer = rsp.getWriter()) {
ResponseResult error = ResponseResult.error(e.getErrorEnum());
String errorResponse = JSONObject.toJSONString(error);
writer.write(errorResponse);
}
return;
}
filterChain.doFilter(encHttpServletRequest,rsp);
}else{
filterChain.doFilter(req,rsp);
}
}
}
這樣,在特定情況下HttpServletRequest
就會wrap成 EncHttpServletRequest
,在endpoint中就可以直接使用:
@PostMapping("login")
public ResponseResult login(EncHttpServletRequest request){
JSONObject originParam = request.getOriginParam();
}
但是這種方式對于參數體都沒有直接在方法中明確,對于使用swagger或者其他接口文檔生成拓展不是很友好。
方案2:AbstractHttpMessageConverter
- 實現
AbstractHttpMessageConverter
可以定義請求類型轉換
/**
* 定義加密http請求參數自定義類型轉換
* 當請求MediaType為application/x-www-form-urlencoded,@RequestBody參數為DecryptParam類型時自動轉換
*/
@Slf4j
public class EncMessageConverter extends AbstractHttpMessageConverter<Object> {
@Autowired
private ObjectMapper objectMapper;
public EncMessageConverter() {
super(MediaType.APPLICATION_FORM_URLENCODED);
}
@Override
protected boolean supports(Class<?> clazz) {
EncryptParam param = clazz.getAnnotation(EncryptParam.class);
return param != null;
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// 解析原始加密參數
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
String originBody = StreamUtils.copyToString(inputMessage.getBody(), charset);
Map<String, Object> parameters = HttpParamUtil.getParameter("?"+originBody);
Object encKey = parameters.get("encKey");
Object params = parameters.get("params");
if(StringUtils.isEmpty(encKey)||StringUtils.isEmpty(params)){
throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM);
}
byte[] aesKey;
try{
/* 1.RSA私鑰解密出AES的KEY */
PrivateKey privateKey = RSAUtil.getPrivateKey(RSAKeys.PRIVATE_KEY);
aesKey = RSAUtil.privateDecrypt((new BASE64Decoder()).decodeBuffer(encKey.toString()), privateKey);
/* 2.AES解密出原始參數 */
String decrypt = AESUtil.decrypt(params.toString(), new String(aesKey));
JavaType javaType = getJavaType(clazz, null);
return this.objectMapper.readValue(decrypt.getBytes(), clazz);
}catch (Exception e){
e.printStackTrace();
throw new GlobalException(ErrorEnum.ERROR_REQUEST_PARAM_DECRYPT);
}
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return super.canRead(clazz, mediaType);
}
@Override
protected void writeInternal(Object request, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
private Charset getContentTypeCharset(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
return contentType.getCharset();
}
else if (contentType != null && contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
// Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
return StandardCharsets.UTF_8;
}
else {
Charset charset = getDefaultCharset();
Assert.state(charset != null, "No default charset");
return charset;
}
}
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
}
}
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}
}
- 創建參數注解
/**
* 標識加密的參數type
* EncMessageConverter進行自動類型轉換
* @see EncMessageConverter
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EncryptParam {
}
- 在原始參數對象上應用注解
@Data
@EncryptParam
public class LoginDTO {
@NotEmpty
private String username;
@NotEmpty
private String password;
@NotEmpty
private String timestamp;
}
這樣一來,滿足條件時,參數將進行自動類型轉換,并且參數文檔也能按照未加密時的原字段生成
@PostMapping("login")
public ResponseResult login(@RequestBody @Valid LoginDTO dto){
}
當然,也可以不使用AbstractHttpMessageConverter
,自行定義注解完成AOP實現