【Java】基于Swagger实现接口混淆

基于Swagger实现接口混淆

前提

要做接口安全设计,保护后端接口安全的安全性,还可以针对接口的请求响应做改变,比如将原本的参数改为一个映射后的值,请求参数先去解析再转成我们的对象,保证我们的接口的含义不能被外部轻易理解,进而维护接口的安全。
既然是基于swagger的自动化混淆,为什么要这么做呢?因为你开发的接口要给人对接,那么对于前端来说,就需要看到正常的请求参数才可以,你加了混淆,没有通知到前端,那么请求参数肯定是不对的,所以就利用了swagger可以生成文档的特性来处理这个混淆的生成。

开发步骤

1.引入swagger的依赖
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <!--此版本支持单接口导出到postman,方便测试-->
    <version>2.0.7</version>
</dependency>
2.添加混淆开关配置
#接口文档
swagger:
  enabled: true
  pathMapping: /
  version: 1.0.0
  # 混淆开关
  confusion:
    switch: false
    
#拦截的不加密的url
filter-encrypt-urls: /v2/api-docs,/swagger,/doc.html,/webjars,/error,/favicon.ico
3.添加swagger配置类
@Configuration
@EnableKnife4j
@EnableSwagger2WebMvc
public class SwaggerConfig {
    @Value("${swagger.enabled}")
    private boolean enabled;
    @Value("${swagger.pathMapping}")
    private String pathMapping;
    @Value("${swagger.version}")
    private String version;

    /**
     * 创建API
     */
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 是否启用Swagger
                .enable(enabled)
                .apiInfo(apiInfo())
                .select()
                // 扫描所有有注解的api,用这种方式更灵活,也可以通过扫描包路径的方式
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                .pathMapping(pathMapping)
                .globalOperationParameters(setHeaderParam());
    }

    /**
     * 设置公共请求头
     *
     * @return
     */
    private List<Parameter> setHeaderParam() {
        List<Parameter> pars = new ArrayList<>();
        ParameterBuilder parameterBuilder = new ParameterBuilder();
        Parameter token = parameterBuilder.name(Constants.TOKEN).description("登录token")
                .modelRef(new ModelRef("string")).parameterType("header")
                .required(false).defaultValue("").build();
        Parameter appVer = parameterBuilder.name(Constants.APP_VERSION).description("APP版本号")
                .modelRef(new ModelRef("string")).parameterType("header")
                .required(true).defaultValue("").build();
        Parameter clientType = parameterBuilder.name(Constants.CLIENT_TYPE).description("客户端类型 安卓=android 苹果=ios")
                .modelRef(new ModelRef("string")).parameterType("header")
                .required(true).defaultValue("").build();
        pars.add(token);
        pars.add(appVer);
        pars.add(clientType);
        return pars;
    }

    /**
     * 全局说明
     * @return
     */
    private static String desc(){
        return "<div>"+"请求说明: 请求参数类型 分为 header body query. " +
                "header 类型数据放到请求头中." +
                " query中参数为 content-type为application/x-www-form-urlencoded." +
                " body 中参数为 content-type为content-type为application/json 注意:(body中传的json数据是:直接传body类型对象参数下级目录中的参数作为json key传递。body类型参数对象本身不需要作为json key传递)" + "</div>";
    }

    /**
     * 添加摘要信息
     */
    private ApiInfo apiInfo()
    {
        // 用ApiInfoBuilder进行定制
        return new ApiInfoBuilder()
                .title("App接口")
                .description("app端接口文档说明:" + desc() )
                .version("版本号:" + version)
                .build();
    }
}
4.添加url拦截器,重写url
@Slf4j
public class UrlMappingFilter extends GenericFilterBean {
    // 去除swagger接口拦截
    public static final String FILTER_URL = "/v2/api-docs,/swagger,/doc.html,/webjars,/error,/favicon.ico";

    @Override
    public void doFilter(ServletRequest httpServletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) httpServletRequest;
        // 去除前缀
        String uri = request.getRequestURI();
        String[] urlList = FILTER_URL.split(",");
        for (String url : urlList) {
            if (uri.contains(url)) {
                chain.doFilter(httpServletRequest, response);
                return;
            }
        }
        ReqParamWrapper replaceRequest = new ReqParamWrapper(request);
        chain.doFilter(replaceRequest, response);
    }
}
5.重写请求参数

众所周知HttpServletRequest 对象没有提供任何的修改方法,所以对于其中的参数来说,只能是读取,不能更新,所以要使用ReqParamWrapper 来重写HttpServletRequest 对象的请求入参。

@Slf4j
public class ReqParamWrapper extends HttpServletRequestWrapper {
    /**
     * 重新赋值 request中的parameterMap中数据
     */
    private Map<String, String[]> params = new HashMap<>();

    private HttpServletRequest httpServletRequest;

    /**
     * 重新赋值request中的文件名称数据
     */
    private Collection<Part> parts = new ArrayList<>();

    @Override
    public Collection<Part> getParts() {
        return parts;
    }

    public ReqParamWrapper(HttpServletRequest request) {
        super(request);
        this.httpServletRequest = request;
        //解密参数名称
        this.decryptParamName(request);
        //解密文件参数名称
        this.decryptFileName(request);
    }

    @Override
    public String getRequestURI() {
        return SimpleEncryption.decryptUrl(httpServletRequest.getRequestURI());
    }

    @Override
    public StringBuffer getRequestURL() {
        StringBuffer url = new StringBuffer();
        String scheme = httpServletRequest.getScheme();
        int port = httpServletRequest.getServerPort();
        if (port < 0) {
            // Work around java.net.URL bug
            port = 80;
        }

        url.append(scheme);
        url.append("://");
        url.append(httpServletRequest.getServerName());
        if ((scheme.equals("http") && (port != 80))
                || (scheme.equals("https") && (port != 443))) {
            url.append(':');
            url.append(port);
        }
        url.append(getRequestURI());
        return url;
    }

    /**
     * 解密参数名称
     *
     * @param request
     */
    private void decryptParamName(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        parameterMap.forEach((k, v) -> {
            this.params.put(SimpleEncryption.decrypt(k, Constants.CONFUSION_PARAMETER), v);
        });
    }

    /**
     * 文件类型数据,解密文件参数名称
     */
    private void decryptFileName(HttpServletRequest request) {
        try {
            if((null == request.getContentType()) || (!request.getContentType().toLowerCase(Locale.ENGLISH).startsWith(FileUploadBase.MULTIPART))){
                return;
            }
            Collection<Part> oldParts = request.getParts();
            if (!CollectionUtils.isEmpty(oldParts)) {
                for (Part part : oldParts) {
                    Object fileItem = ReflectUtil.getFieldValue(part, "fileItem");
                    Object fieldNameValue = ReflectUtil.getFieldValue(fileItem, "fieldName");
                    if (fieldNameValue == null) {
                        continue;
                    }
                    ReflectUtil.setFieldValue(fileItem, "fieldName", SimpleEncryption.decrypt((String) fieldNameValue, Constants.CONFUSION_PARAMETER));
                    parts.add(part);
                }
            }
        } catch (Exception e) {
            log.error("解密文件名失败", e);
        }
    }

    public ReqParamWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
        this(request);
        addParameters(extraParams);
    }

    public void addParameters(Map<String, Object> extraParams) {
        for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
            addParameter(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public Enumeration<String> getParameterNames() {
        Set<String> set = params.keySet();
        if (set.size() > 0) {
            return Collections.enumeration(set);
        } else {
            return super.getParameterNames();
        }
    }

    @Override
    public String getParameter(String name) {
        String[] values = params.get(name);
        if (values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }

    @Override
    public String[] getParameterValues(String name) {
        return params.get(name);
    }

    public void addParameter(String name, Object value) {
        if (value != null) {
            if (value instanceof String[]) {
                params.put(name, (String[]) value);
            } else if (value instanceof String) {
                params.put(name, new String[]{(String) value});
            } else {
                params.put(name, new String[]{String.valueOf(value)});
            }
        }
    }
}
7.注册拦截器

我们使用的是Ruoyi框架,所以基于Spring security,只需要注册我们的拦截器就可以了

// 2、spring security修改加密url,新增swagger过滤器
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {

    // 注解标记允许匿名访问的url
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
    permitAllUrl.getUrls().forEach(url -> {
                if (confusionSwitch) {
                    String decrypt = SimpleEncryption.encryptUrl(url);
                    registry.antMatchers(decrypt).permitAll();
                } else {
                    registry.antMatchers(url).permitAll();
                }
            }
    );

    httpSecurity
            // CSRF禁用,因为不使用session
            .csrf().disable()
            // 认证失败处理类
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .antMatchers(confusionSwitch ? SimpleEncryption.encryptUrl("/api/user/register") : "/api/user/register").anonymous()
            // 静态资源,可匿名访问
            .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
            .antMatchers("/webjars/**", "/*/api-docs", "/druid/**").permitAll()
            .antMatchers("/swagger-resources/configuration/ui", // 用来获取支持的动作
                    "/swagger-resources", // 用来获取api-docs的URI
                    "/swagger-resources/configuration/security", // 安全选项
                    "/swagger-resources/**").permitAll()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated()
            .and()
            .headers().frameOptions().disable();
    // 添加Logout filter
    httpSecurity.logout().logoutUrl("/signOut").logoutSuccessHandler(logoutSuccessHandler);
    // 添加JWT filter
    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    httpSecurity.addFilterBefore(corsFilter, AppJwtAuthenticationTokenFilter.class);
    httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    if (confusionSwitch) {
        httpSecurity.addFilterBefore(new UrlMappingFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}
8. 重写请求体参数
@RestControllerAdvice
@Slf4j
public class EncryptResponseBodyAdapter implements ResponseBodyAdvice<Object> {

    @Value("${swagger.confusion.switch:true}")
    private Boolean confusionSwitch;

    /**
     * 该方法用于判断当前请求的返回值,是否要执行beforeBodyWrite方法
     *
     * @param methodParameter handler方法的参数对象
     * @param converterType   将会使用到的Http消息转换器类类型
     * @return 返回true则会执行beforeBodyWrite
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    /**
     * 在Http消息转换器执转换,之前执行
     *
     * @param body               服务端的响应数据
     * @param methodParameter    handler方法的参数对象
     * @param mediaType          响应的ContentType
     * @param converterType      将会使用到的Http消息转换器类类型
     * @param serverHttpRequest  serverHttpRequest
     * @param serverHttpResponse serverHttpResponse
     * @return 返回 一个自定义的HttpInputMessage,可以为null,表示没有任何响应
     */
    @Override
    @Nullable
    public Object beforeBodyWrite(@Nullable Object body, MethodParameter methodParameter, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        String requestURI = ServletUtils.getRequest().getRequestURI();
        log.info("处理请求地址-响应:{}", requestURI);
        //获取请求数据
        String srcData = JSON.toJSONString(body);
        if (!requestURI.contains("v2/api-docs")) {
            log.info("响应body={}", srcData);

        }
        if (!confusionSwitch) {
            stopWatch.stop();
            log.info("加密响应体耗时:【{}】ms", stopWatch.getLastTaskTimeMillis());
            return body;
        }
        Object obj = changeJsonBody(body, requestURI);
        stopWatch.stop();
        log.info("加密响应体耗时:【{}】ms", stopWatch.getLastTaskTimeMillis());
        return obj;
    }
    
    /**
     *
     * 对json数据key 进行混淆加密
     *
     */
    public Object changeJsonBody(Object body, String requestURI) {
        if (Objects.isNull(body)) {
            return null;
        }
        if (filterEncryptUrl(requestURI)) {
            return body;
        }
        JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(body, JSONWriter.Feature.WriteMapNullValue));
        Object o = jsonObject.get("data");
        if (o instanceof CharSequence || o instanceof Boolean || o instanceof Number || Objects.isNull(o)) {
            return jsonObject;
        }
        String result = SimpleEncryption.handleEncryptReplaceStr(JSON.toJSONString(o, JSONWriter.Feature.WriteMapNullValue));
        if (JSONUtil.isJson(result)) {
            if (JSONUtil.isJsonArray(result)) {
                JSONArray objects = JSON.parseArray(result);
                log.info("返回加密后新参数body:{}", objects);
                jsonObject.put("data", objects);
            }
            if (JSONUtil.isJsonObj(result)) {
                JSONObject object = JSON.parseObject(result);
                log.info("返回加密后新参数body:{}", object);
                jsonObject.put("data", object);
            }
        } else {
            jsonObject.put("data", result);
        }
        return jsonObject;
    }

    /**
     * 过滤路径
     */
    private static boolean filterEncryptUrl(String uri) {
        String filterEncryptUrls = GlobalConfig.getConfig("filter-encrypt-urls");
        String[] filterEncryptUrlStr = filterEncryptUrls.split(",");
        for (String filterEncryptUrl : filterEncryptUrlStr) {
            if (uri.contains(filterEncryptUrl)) {
                return true;
            }
        }
        return false;
    }
}
9.重写响应体参数

/**
 * <p>
 * 接口入参解密
 * </p>
 * RequestBodyAdvice可以理解为在@RequestBody之前需要进行的 操作,<br/>
 * ResponseBodyAdvice可以理解为在@ResponseBody之后进行的操作;<br/>
 * 所以当接口需要加解密时,在使用@RequestBody接收前台参数之前可以先在RequestBodyAdvice的实现类中进行参数的解密,<br/>
 * 当操作结束需要返回数据时,可以在@ResponseBody之后进入ResponseBodyAdvice的实现类中进行参数的加密。<br/>
 */
@RestControllerAdvice
@Slf4j
public class DecryptRequestBodyAdapter extends RequestBodyAdviceAdapter {

    @Value("${swagger.confusion.switch:true}")
    private Boolean confusionSwitch;

    /**
     * 该方法用于判断当前请求,是否要执行beforeBodyRead方法
     *
     * @param methodParameter handler方法的参数对象
     * @param targetType      handler方法的参数类型
     * @param converterType   将会使用到的Http消息转换器类类型
     * @return 返回true则会执行beforeBodyRead
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
          Class<? extends HttpMessageConverter<?>> converterType) {
       // 开关判断
       return true;
    }

    /**
     * 在Http消息转换器执转换,之前执行
     *
     * @param inputMessage    客户端的请求数据
     * @param methodParameter handler方法的参数对象
     * @param targetType      handler方法的参数类型
     * @param converterType   将会使用到的Http消息转换器类类型
     * @return 返回 一个自定义的HttpInputMessage
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter,
          Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

       String requestURI = ServletUtils.getRequest().getRequestURI();
       log.info("处理请求地址-请求:{}", requestURI);
       // 获取请求数据
       if (!confusionSwitch) {
          //解密混淆
          return inputMessage;
       }

       // 读取加密的请求体
       InputStream body = inputMessage.getBody();
       HttpHeaders headers = inputMessage.getHeaders();
       String s = getBodyString(body);
       if (!ObjectUtils.isEmpty(body)) {
          body.close();
       }
       headers.remove("Content-Length");
       log.info("解密前-body:{}", s);
       if (Strings.isNotEmpty(s)) {
          String bodyDec = SimpleEncryption.handleDecryptReplaceStr(s);
          log.info("解密后-body:{}", bodyDec);
          if (Strings.isNotEmpty(bodyDec)) {
             // 使用解密后的数据,构造新的读取流
             InputStream inputStream = new ByteArrayInputStream(
                   changeJsonBody(bodyDec, requestURI).getBytes(StandardCharsets.UTF_8));
             return new HttpInputMessage() {
                @Override
                public HttpHeaders getHeaders() {
                   return inputMessage.getHeaders();
                }

                @Override
                public InputStream getBody() {
                   return inputStream;
                }
             };
          }
       }

       return inputMessage;
    }

    private String getBodyString(InputStream inputStream) {
       ByteArrayOutputStream result = null;
       try {
          result = new ByteArrayOutputStream();
          byte[] buffer = new byte[1024];
          int length;
          while ((length = inputStream.read(buffer)) != -1) {
             result.write(buffer, 0, length);
          }
          return result.toString("UTF-8");
       } catch (Exception e) {
          log.error("getBodyString", e);
       } finally {
          if (result != null) {
             try {
                result.close();
             } catch (IOException e) {
                log.error("getBodyString close", e);
             }
          }
       }
       return "";
    }

    /**
     *
     * 对json数据key进行替换前m后n处理
     *
     */
    public String changeJsonBody(String body, String requestURI) {
       CommonParam commonParam = ServletUtils.getCommonParam();
       String path = commonParam.getPath();
       if (StringUtils.isEmpty(path)) {
          return body;
       }
       return body;
    }
}
10.附件工具类
public class SuperJsonUtil {
    /**
     * 加密json key
     *
     * @param jsonObject json对象
     * @return {@link JSONObject}
     */
    private static JSONObject encryptKeys(JSONObject jsonObject) {
        JSONObject outputJson = new JSONObject();
        for (String key : jsonObject.keySet()) {
            Object value = jsonObject.get(key);
            String newKey = SimpleEncryption.encrypt(key, Constants.CONFUSION_PARAMETER);  // the logic of new key
            if (value instanceof JSONObject) {
                outputJson.put(newKey, encryptKeys((JSONObject) value));
            } else if (value instanceof JSONArray) {
                outputJson.put(newKey, encryptKeysInArray((JSONArray) value));
            } else {
                outputJson.put(newKey, value);
            }
        }
        return outputJson;
    }

    /**
     * 加密json数组 key
     *
     * @param jsonArray json数组
     * @return {@link JSONArray}
     */
    private static JSONArray encryptKeysInArray(JSONArray jsonArray) {
        JSONArray outputArray = new JSONArray();
        for (int i = 0; i < jsonArray.size(); i++) {
            Object item = jsonArray.get(i);
            if (item instanceof JSONObject) {
                outputArray.add(encryptKeys((JSONObject) item));
            } else {
                outputArray.add(item);
            }
        }
        return outputArray;
    }

    /**
     * 解密json的key
     *
     * @param jsonObject json对象
     * @return {@link JSONObject}
     */
    private static JSONObject decryptKeys(JSONObject jsonObject) {
        JSONObject outputJson = new JSONObject();
        for (String key : jsonObject.keySet()) {
            Object value = jsonObject.get(key);
            String newKey = SimpleEncryption.decrypt(key, Constants.CONFUSION_PARAMETER);  // the logic of new key
            if (value instanceof JSONObject) {
                outputJson.put(newKey, decryptKeys((JSONObject) value));
            } else if (value instanceof JSONArray) {
                outputJson.put(newKey, decryptKeysInArray((JSONArray) value));
            } else {
                outputJson.put(newKey, value);
            }
        }
        return outputJson;
    }

    /**
     * 解密json数组的key
     *
     * @param jsonArray json数组
     * @return {@link JSONArray}
     */
    private static JSONArray decryptKeysInArray(JSONArray jsonArray) {
        JSONArray outputArray = new JSONArray();
        for (int i = 0; i < jsonArray.size(); i++) {
            Object item = jsonArray.get(i);
            if (item instanceof JSONObject) {
                outputArray.add(decryptKeys((JSONObject) item));
            } else {
                outputArray.add(item);
            }
        }
        return outputArray;
    }

    public static String handleDecryptReplaceStr(String json) {
        if (StringUtils.isEmpty(json)) {
            return null;
        }
        if (!JSONUtil.isJson(json)) {
            return json;
        }
        if (JSONUtil.isJsonObj(json)) {
            return decryptKeys(JSON.parseObject(json)).toString();
        }
        if (JSONUtil.isJsonArray(json)) {
            return decryptKeysInArray(JSON.parseArray(json)).toString();
        }
        return json;
    }

    public static String handleEncryptReplaceStr(String json) {
        if (StringUtils.isEmpty(json)) {
            return null;
        }
        if (!JSONUtil.isJson(json)) {
            return json;
        }
        if (JSONUtil.isJsonObj(json)) {
            return encryptKeys(JSON.parseObject(json)).toString();
        }
        if (JSONUtil.isJsonArray(json)) {
            return encryptKeysInArray(JSON.parseArray(json)).toString();
        }
        return json;
    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值