Springboot統一參數解密的一些方案

在項目開發中,很多場景下我們的接口參數都需要進行加解密處理。

例如我開發的此項目中,參數原文使用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

  1. 定義一個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);
        }
    }
}
  1. 再定義一個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

  1. 實現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);
        }
    }
}

  1. 創建參數注解
/**
 * 標識加密的參數type
 * EncMessageConverter進行自動類型轉換
 * @see EncMessageConverter
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EncryptParam {

}

  1. 在原始參數對象上應用注解
@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實現

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370