1. 程式人生 > 其它 >基於SpringBoot實現資料許可權驗證

基於SpringBoot實現資料許可權驗證

1.實現方案

方案很簡單:對需要進行資料許可權的請求新增自定義註解,通過攔截器對請求進行攔截,判斷是否需要進行資料許可權驗證和執行資料許可權驗證的邏輯。(GET請求沒問題,POST請求因為HttpRequest的流getReader只能讀取一次,如果在攔截器處理後,進入Handler會拋異常。此問題後面單獨說

2.程式碼實現

2.1 抽象資料許可權驗證類

/**
 * @Description 抽象的資料許可權類
 * @Author zouxiaodong
 * @Date 2022/03/01 15:36
 */
public abstract class AbstractDataAuth {

    /**
     * @Author zouxiaodong
     * @Description 資料許可權控制邏輯
     * @Date 2022/03/01 15:42
     * @Param [httpServletRequest, httpServletResponse]
     * @return boolean
     **/
    public abstract boolean checkDataAuth(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse);
}

2.2 自定義註解,增加在需要進行資料許可權驗證的Handler上

/**
 * @Author zouxiaodong
 * @Description 資料許可權認證的註解
 * @Date 2022/03/01 15:16
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAuthValid {

    /**
     * 名稱
     **/
    String name() default "";

    /**
     * 資料許可權開關,預設開啟
     **/
    boolean switchAuth() default true;

    /**
     * 資料許可權處理類
     **/
    Class<? extends AbstractDataAuth> dataAuthClass();
}

2.3 自定義攔截器,判斷請求對應的handler是否有註解(是否需要進行許可權驗證)

/**
 * @Description 資料許可權攔截器
 * @Author zouxiaodong
 * @Date 2022/03/01 16:18
 */
@Component
@Slf4j
public class DataAuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
        try {
            // 如果不是方法
            if(!(handler instanceof HandlerMethod)){
                return true;
            }
            DataAuthValid dataAuthValid = ((HandlerMethod) handler).getMethodAnnotation(DataAuthValid.class);
            if(dataAuthValid == null || !dataAuthValid.switchAuth()){
                return true;
            }else{
                Class<? extends AbstractDataAuth> dataAuthClass = dataAuthValid.dataAuthClass();
//                return dataAuthClass.newInstance().checkDataAuth(request,response);
                return SpringUtil.getBean(dataAuthClass).checkDataAuth(request,response);
            }
        }catch (Exception e){
            log.error("DataAuthInterceptor執行請求:{}攔截異常。異常資訊為:{}",request.getRequestURI(),e.getMessage());
            return false;
        }
    }
}

2.4 自定義Filter(對特定請求的HttpRequest進行Wrapper處理,避免Post請求對流進行二次讀取時的異常)

/**
 * @Description 需要對post或者put請求進行資料許可權驗證時的filter
 * @Author zouxiaodong
 * @Date 2022/03/02 16:07
 */
@Slf4j
public class PostMethodFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        CustomHttpServletRequestWrapper customHttpServletRequestWrapper = null;
        HttpServletRequest req = (HttpServletRequest)request;
        try {
            customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(req);
        }catch (Exception e){
            log.warn("請求({})執行filter異常。異常資訊為:{}",req.getRequestURI(), e.getMessage());
        }
        chain.doFilter((Objects.isNull(customHttpServletRequestWrapper) ? request : customHttpServletRequestWrapper), response);
    }
}

2.5 自定義HttpServletRequestWrapper,對HttpRequest進行處理

/**
 * @Description 自定義請求wrapper
 * @Author zouxiaodong
 * @Date 2022/03/02 10:34
 */
@Slf4j
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private byte[] body;

    public byte[] getBody() {
        return body;
    }

    public String getBodyAsString(){
        return new String(body,StandardCharsets.UTF_8);
    }

    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        StringBuilder sb = new StringBuilder();
        String line;
        BufferedReader reader = request.getReader();
        while ((line=reader.readLine()) != null){
            sb.append(line);
        }
        this.body = sb.toString().getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public void setReadListener(ReadListener listener) {
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public boolean isFinished() {
                return false;
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

2.6 針對自定義的Interceptor和Filter進行系統配置(如果系統中有swagger,新增自定義攔截器後可能會導致swagger不可用,增加註釋的兩行程式碼即可)

/**
 * @Description 資料許可權filter配置
 * @Author zouxiaodong
 * @Date 2022/03/01 16:36
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean servletRegistrationBean() {
        PostMethodFilter postMethodFilter = new PostMethodFilter();
        FilterRegistrationBean<PostMethodFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(postMethodFilter);
        bean.setName("postMethodFilter");
        bean.addUrlPatterns("/snapshot/updatePolicy");
        bean.setOrder(Ordered.LOWEST_PRECEDENCE);
        return bean;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        //對所有請求進行攔截
        registry.addInterceptor(new DataAuthInterceptor()).addPathPatterns("/**").
                //解決swagger無法訪問的問題
                excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
    }

    //解決swagger無法訪問的問題
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

}

2.7 將註解加到需要進行資料許可權驗證的方法上

 /**
     * @Author zouxiaodong
     * @Description 更新虛擬機器的快照策略(如果之前沒有策略則新增,有則更新策略)
     * @Date 2021/12/08 9:51
     * @Param [vmID, snapshotPolicy]
     * @return com.zkxy.common.returnUtil.OperationResult<java.lang.Boolean>
     **/
    @ApiOperation("更新虛擬機器的快照策略(如果之前沒有策略則新增,有則更新策略;如果之前有策略,新的策略為空則進行策略禁用或停止)")
    @RequestMapping(value = "/updatePolicy",method = RequestMethod.POST)
    @DataAuthValid(name = "updatePolicy",switchAuth = true,dataAuthClass = UpdatePolicyDataAuth.class)
    public OperationResult<Boolean> updatePolicy(@ApiParam(name = "policy",value = "新的虛擬機器快照策略",required = true) @RequestBody UpdateSnapshotPolicy policy){
        ......
    }

2.8 實現註解中具體資料許可權認證類

/**
 * @Description 快照策略升級的資料許可權認證
 * @see VMSnapshotController#updatePolicy(com.zkxy.eda.vmware.vcenter.pojo.UpdateSnapshotPolicy)
 * @Author zouxiaodong
 * @Date 2022/03/02 9:15
 */
@Slf4j
@Component
public class UpdatePolicyDataAuth extends AbstractDataAuth{

    @Autowired
    private VirtualMachineService virtualMachineService;

    @Override
    public boolean checkDataAuth(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        if(RequestMethod.POST.name().equals(httpServletRequest.getMethod())){
            try{
                String body = ((CustomHttpServletRequestWrapper)httpServletRequest).getBodyAsString();
                UpdateSnapshotPolicy updateSnapshotPolicy = JSONObject.parseObject(body, UpdateSnapshotPolicy.class);
                if(updateSnapshotPolicy == null){
                    return false;
                }
                String vmId = updateSnapshotPolicy.getVmId();
                List<ZkxyVirtualMachine> zkxyVirtualMachines = virtualMachineService.getVirtualMachinesByUserSession();
                if(!CollectionUtils.isEmpty(zkxyVirtualMachines)){
                    for (ZkxyVirtualMachine zkxyVirtualMachine:zkxyVirtualMachines){
                        if(zkxyVirtualMachine.getVmId().equals(vmId)){
                            return true;
                        }
                    }
                }
                httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                OperationResult<ZkxyVirtualMachine> result = OperationResult.fail(null,String.format("您沒有許可權訪問此雲主機(%s)",vmId),null);
                httpServletResponse.setHeader("Content-Type","application/json;charset=UTF-8");
                httpServletResponse.getWriter().write(JSONObject.toJSONString(result));
                httpServletResponse.getWriter().flush();
                log.warn("請求({})已拒絕!vmId:{}",httpServletRequest.getRequestURI(),vmId);
            }catch (Exception e){
                log.error("請求({})處理異常.異常資訊為:{}",httpServletRequest.getRequestURI(),e.getMessage());
            }
        }
        return false;
    }
}

3.開發過程中的問題

一開始的時候只自定義了Interceptor,在對POST請求攔截器中讀取了HttpRequest的getReader,導致執行到Handler時,系統提示:

java.lang.IllegalStateException: getReader() has already been called for this request
    org.apache.catalina.connector.Request.getInputStream(Request.java:1032)
    org.apache.catalina.connector.RequestFacade.getInputStream(RequestFacade.java:364)

此時就需要自定義Filter(步驟2.4)對需要的請求進行HttpRequest處理(步驟2.5),轉為HttpServletRequestWrapper,然後將該HttpServletRequestWrapper放入過濾鏈中傳遞下去。