在项目开发中,很多场景下我们的接口参数都需要进行加解密处理。
例如我开发的此项目中,参数原文使用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实现