基于springmvc的通用数据权限校验(鉴权)框架——使用示例

最近在开始写【spring源码深度解析】系列文章,如果有对spring源码感兴趣的同学,欢迎点击左上角我的头像,查看相关文章,该系列文章均是结合我当时学习源码的经历编写完成,不是一上来就堆砌源码,而是从日常使用展开学习,力求让初学者弄懂spring相关模块的实现

基于springmvc的通用数据权限校验框架——使用示例

前言

首先提出一个问题:在后台管理系统中,我们一般会根据当前登录用户查出直属相关的数据做列表展示,列表中有一列都会供用户点击来查看详情或其他信息,比如我是个保险公司的销售员,我要看我签约了哪些客户,然后点击某一个客户的详情看看签的合同细节,大家一般会怎么做?

有些同学可能会这么实现(采用springboot,代码只是基本实现,忽律空指针等异常)

  • 首先是登录:

    代码实现(简单示意)
	//模仿redis存储登录用户token及用户信息
    public static Map<String, Map<String,Object>> redisMap =new HashMap<>(128);
	@Autowired
    private JdbcTemplate jdbcTemplate;
    
    @RequestMapping("/login")
    @ResponseBody
    public WebResult login(String username, String password){
        //密码就不做加解密了
        List<Map<String, Object>> userMapList=jdbcTemplate.queryForList("select * from t_user where name=? and password=?",username,password);
        if (userMapList==null || userMapList.size()==0){
            return WebResult.errorWebResult("用户名或密码错误");
        }
        Map<String,Object> dd=new HashMap<>();
        dd.put("userId",userMapList.get(0).get("id"));
        String token="test-"+ UUID.randomUUID().toString();
        redisMap.put(token,dd);
        return WebResult.successWebResult(token);
    }
  • 当前业务员发展的客户列表

    代码实现
	@Autowired
    private JdbcTemplate jdbcTemplate;

    @RequestMapping("/customerList")
    @ResponseBody
    public WebResult customerList(String token){
    	Integer userId = (Integer) Application.redisMap.get(token).get("userId");
        //根据当前用户查出
        return WebResult.successWebResult(jdbcTemplate.queryForList("select * from t_customer where user_id=?",userId));
    }
  • 点击【合同详情】
    在这里插入图片描述
    代码实现
    (前端使用的layui,这里不重要,只需要知道前端调用详情的接口时传了客户id到后台)
	layui.use('table', function(){
        var table = layui.table;

        table.render({
            elem: '#test'
            ,url:'http://localhost:8080/customer/customerList'
            ,where: {token: sessionStorage.getItem("token")}
            ,cols: [[
                {field:'id', width:80, title: 'ID', sort: true}
                ,{field:'username', width:80, title: '用户名'}
                ,{field:'sex', width:80, title: '性别', sort: true,templet: function(d){
                        return d.sex==1?'男':'女';
                    }}
                ,{field:'city', width:80, title: '城市'}
                ,{field:'sign', width:80, title: '签名'}
                ,{field:'classify', width:120,title: '职业'}
                ,{field:'wealth', title: '更多操作', sort: true,templet: '<div><a href="javascript:showContractInfo({{d.id}})"  class="layui-table-link">合同详情</a></div>'}
            ]]
        });
    });

	function showContractInfo(customerId) {
        var htmls='<form class="layui-form" action="http://localhost:8080/customer/contractModify" lay-filter="example" id="form">' +
            '  <input type="text" name="id" id="id" style="display: none">' +
            '  <div class="layui-form-item">' +
            '    <label class="layui-form-label">合同标题</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="text" name="tile" id="tile" lay-verify="title" autocomplete="off"  class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item">' +
            '    <label class="layui-form-label">合同内容</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="text" name="content" id="content"  autocomplete="off" class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item">' +
            '    <label class="layui-form-label">保费金额</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="number" name="amount" id="amount"  autocomplete="off" class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item">' +
            '    <label class="layui-form-label">签约人</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="text" name="signPerson" id="signPerson"  autocomplete="off" class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item">' +
            '    <label class="layui-form-label">身份证号码</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="text" name="identityNo" id="identityNo"  autocomplete="off" class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item layui-form-text">' +
            '    <label class="layui-form-label">联系电话</label>' +
            '    <div class="layui-input-block">' +
            '      <input type="text" name="linkmanPhone" id="linkmanPhone"  autocomplete="off" class="layui-input">' +
            '    </div>' +
            '  </div>' +
            '  <div class="layui-form-item">' +
            '    <div class="layui-input-block">' +
            '      <button type="submit" class="layui-btn" lay-submit="" lay-filter="demo1">立即提交</button>' +
            '    </div>' +
            '  </div>' +
            '</form>';
        layer.open({
            type:1,
            content: htmls,
            success: function(layero, index){
                senAjax({
                    url: 'http://localhost:8080/customer/contractInfo',
                    data: {
                        customerId:customerId
                    },
                    dataType: "json",
                    success: function(resp){
                        if (resp.code == 0) {
                            var np=$("#form");
                            $.each(resp.data,function(key,values){
                                np.find("#"+key).val(values);
                            });
                        }
                    }
                })
            }
        })
    }

	//查看合同详情
	@RequestMapping("/contractInfo")
    @ResponseBody
    public WebResult contractInfo(String customerId){
        return WebResult.successWebResult(jdbcTemplate.queryForMap("select * from t_contract where customerId=?",customerId));
    }
    //修改合同
	@RequestMapping("/contractModify")
    public String contractModify(String id,String tile,String content,String signPerson,String identityNo,String linkmanPhone){
        String sql ="update t_contract set tile=?,content=?,signPerson=?,identityNo=?,linkmanPhone=? where id=?";
        jdbcTemplate.update(sql,tile,content,signPerson,identityNo,linkmanPhone,id);
        return "index";
    }

这样的实现看起来并没有什么问题,功能一切正常,但存在安全隐患,contractInfo接口是直接根据客户编号去对应表里查,并没有校验传过来的客户编号是属于当前登录用户的!!!
如果有人这么操作
在这里插入图片描述
在这里插入图片描述
这样别人就可以通过这个接口随便传customerId,最终获取整个系统的客户的合同信息,像这个接口就返回了手机号,身份证号等敏感信息,客户可能会被恶意骚扰或者被一些公司贩卖信息,这是坚决不行的!!!
观察仔细的同学还会发现contractModify接口也存在同样的问题,而且更严重,可以任意修改t_contract表的数据!!!

大家可以暂停下来看看自己负责的项目中有没有类似的问题

如何解决: 以contractInfo接口为例,修改后的逻辑如下

	public WebResult contractInfo(String token, String customerId) {
		//先获取当前登录的用户id,在关联customerId去t_customer中查,如果没有就返回错误
        Integer userId = (Integer) Application.redisMap.get(token).get("userId");
        int count = jdbcTemplate.queryForObject("select count(*) from t_customer where id=? and user_id=?", Integer.class, customerId, userId);
        if (count==0) {
            return WebResult.errorWebResult("查询数据不存在");
        }
        return WebResult.successWebResult(jdbcTemplate.queryForMap("select * from t_contract where customerId=?", customerId));
    }

contractModify接口是先查出customerId,然后和上面的校验逻辑一致
现在接口有了鉴权,消除了安全隐患。但是大点的项目肯定不只这一点接口,可能还有给客户发短信等一系列以客户为主体的接口,和以合同为主体的接口,那么上面鉴权的代码就会复制多份,哪怕封装到service里,也是每个方法都会调用一次;如果来了个新同事,开发一个合同失效的新接口,可能就会忘记调用service的鉴权方法,如果开发时间紧张,老员工也可能会忘记做鉴权。而且鉴权的代码其实在一些修改功能的接口里是多余的 ,这里的意思是割裂了主要业务代码,接口里的代码应当围绕在修改的逻辑。

有没有通用一点的解决方案呢?可以基于注解来做鉴权,本人已经封装成了一个框架,稍加配置即可使用

如何使用

该框架参照 @RequestMapping 的实现方式,使用特定注解标明需要校验的参数名和实现校验逻辑的处理器bean的name,标注在类或方法上,对符合条件的方法进行拦截,若处理器校验通过,则调用链继续,否则抛出特定异常。

  • pom.xml新增依赖(由于jar包未上传到外网maven仓库,需要将项目从github (https://github.com/dingmengyang/ SecurityFramework)拉到本地,install到本地maven仓库,若失败,请参照这篇文章https://blog.csdn.net/gao_zhennan/ article/details/89713407
		<dependency>
            <groupId>org.jason</groupId>
            <artifactId>data-permission-check</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
  • 修改代码:

【controller】

	//【parameterName表示要拦截的参数名,resolverName表示处理具体鉴权逻辑的bean name】
	@DataPermission(parameterName = "customerId",resolverName = "customerDataPermissionResolver")
    @RequestMapping("/contractInfo")
    @ResponseBody
    public WebResult contractInfo(String token, String customerId) {
        return WebResult.successWebResult(jdbcTemplate.queryForMap("select * from t_contract where customerId=?", customerId));
    }

【配置类】

@Configuration
@ControllerAdvice
public class Config extends WebMvcConfigurerAdapter implements ApplicationContextAware {
	//鉴权框架需要根据@DataPermission的resolverName参数从applicationContext获取对应bean
    private ApplicationContext applicationContext;

	//【处理权限校验异常】也可以通过HandlerExceptionResolver实现
    @ExceptionHandler(value = {DataPermissionException.class})
    @ResponseBody
    public WebResult exceptionHandler(DataPermissionException e){
        return WebResult.exceptionWebResult(Integer.parseInt(e.getCode()),e.getErrorMessage());
    }
	//【添加特定的权限拦截器DataPermissionCheckInterceptor】
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	//拦截器构造参数需要传DataPermissionResolverContainer(缓存method与处理器bean的对应关系),框架提供了两个默认实现类
    	//1.InitializingDataPermissionResolverContainer,即在启动的时候就遍历所有controller,把符合条件的method缓存
    	//2.SimpleDataPermissionResolverContainer,启动时不做任何处理,当method被调用时,若符合条件,则会被缓存
        registry.addInterceptor(new DataPermissionCheckInterceptor(new InitializingDataPermissionResolverContainer(applicationContext)));
    }
	//【重点!!!添加鉴权处理器,方法内用到的service或者其他类请通过参数的形式传入,例如这里的JdbcTemplate】
	//即@DataPermission的resolverName所指的bean,这里我把contractInfo接口鉴权的逻辑放在了这
    @Bean
    public DataPermissionResolver customerDataPermissionResolver(@Autowired JdbcTemplate jdbcTemplate){
        return new DataPermissionResolver() {
        	//返回true表示有权限,返回false则会抛出DataPermissionException异常
            @Override
            public boolean hasDataPermission(HttpServletRequest httpServletRequest, Object parameter) {
            	//parameter即@DataPermission的parameterName在前端参数里的值
            	//如果前端没传拦截的参数或者参数为空,返回true,这里可根据具体情况具体处理
                if (parameter==null || StringUtils.isEmpty(parameter.toString())){
                    return true;
                }
                String customerId=parameter.toString();
                String token=httpServletRequest.getParameter("token");
                Integer userId = (Integer) Application.redisMap.get(token).get("userId");
                int count = jdbcTemplate.queryForObject("select count(*) from t_customer where id=? and user_id=?", Integer.class, customerId, userId);
                return count>0;
            }
        };
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }


}

【前端】

function senAjax(data) {
    if (!data.data) {
        data.data={};
    }
    data.data.token=sessionStorage.getItem("token");
    $.ajax({
        url: data.url,
        data:data.data,
        dataType: data.dataType || "json",
        success: function(resp){
        	//DataPermissionException错误码默认9527,ErrorMessage默认是【数据无法访问】,可在异常捕获配置那自行修改,前后端一致即可
            if (resp.code == 9527) {
                layer.closeAll();
                layer.alert(resp.msg);
                return;
            }
            data.success(resp);
        }
    })
}

现在我们再来篡改一下前端页面来试下
在这里插入图片描述
ok!现在系统已经接入了框架,只需要通过注解就能实现鉴权,还有一个contractModify接口相信大家应该知道怎么弄了吧!

流程图

下面结合流程图具体介绍下框架的逻辑

Created with Raphaël 2.2.0 http请求 DataPermissionCheckInterceptor#preHandle 1.Method或者其Class上 有DataPermission注解? 2.注解的forceCheck属性值为true或者 parameterName属性值 在Method的参数列表里? 注解的resolverName属性值 的bean#hasDataPermission 调用链继续 抛出DataPermissionException yes no yes no

重点说下第三步:

  1. @DataPermission可标注在类和方法上,都有时以方法上的注解为准
  2. @DataPermission的forceCheck属性默认为false;为false需要判断注解方法的参数数组里是否有parameterName同名参数,有才获取对应resolver并调用hasDataPermission方法,像这种拦截的参数名是id,而方法没有id这个参数,这种情况框架不会做鉴权
	@DataPermission(parameterName = "id",resolverName = "customerDataPermissionResolver")
    @RequestMapping("/contractInfo")
    @ResponseBody
    public WebResult contractInfo(String token, String customerId) {
    	...
    }

为true则跳过判断,适合当校验参数在封装类中时使用,比如

	//forceCheck = true则一定会鉴权
	@DataPermission(parameterName = "customerId",resolverName = "customerDataPermissionResolver",forceCheck = true)
    @RequestMapping("/contractInfo1")
    @ResponseBody
    public WebResult contractInfo1(QueryDto dto) {
        return WebResult.successWebResult(jdbcTemplate.queryForMap("select * from t_contract where customerId=?", dto.getCustomerId()));
    }
public class QueryDto {

    private String token;
    private String customerId;
}

使用该框架的好处

  1. 方法里的代码更简洁,逻辑更清晰
  2. 一般controller都是以某个业务主体为纬度命名,比如CustomerController,里面的方法也基本上都是和Customer相关的,都会有一个customerId名称的参数,所以只需在类加上 @DataPermission(parameterName = “customerId”,resolverName = “xxDataPermissionResolver”)即可,后面新写的接口只需按约定把方法的一个参数命名为customerId即可,如果命名为了customerNo,那么这个接口就不在框架作用范围内了,可以在该方法上使用@DataPermission(parameterName = “customerNo”,resolverName = “xxDataPermissionResolver”)

附录

  • 项目地址

示例项目地址
鉴权框架项目地址

  • spring项目如何使用

xml配置:

	<!--注册拦截器-->
 	<bean name="dataPermissionCheckInterceptor" class="org.jason.datapermissioncheck.DataPermissionCheckInterceptor">
        <constructor-arg name="dataPermissionResolverContainer" ref="dataPermissionResolverContainer"/>
    </bean>

    <bean name="dataPermissionResolverContainer" class="com.example.web.base.MyDataPermissionResolverContainer"/>

直接在xml里注册框架现有的DataPermissionResolverContainer实现类需要给构造参数传ApplicationContext,总不能重新弄个ApplicationContext,所以后面通过封装类来实现

public class MyDataPermissionResolverContainer implements DataPermissionResolverContainer, ApplicationContextAware {

    private DataPermissionResolverContainer delegate;

    @Override
    public void addResolver(String s, DataPermissionResolver dataPermissionResolver) {
        delegate.addResolver(s,dataPermissionResolver);
    }

    @Override
    public void removeResolver(String s) {
        delegate.removeResolver(s);
    }

    @Override
    public DataPermissionResolver getResolver(Method method, Class<?> aClass) {
        return delegate.getResolver(method,aClass);
    }

    @Override
    public void clear() {
        delegate.clear();
    }

    @Override
    public DataPermission getDataPermission(Method method, Class<?> aClass) {
        return delegate.getDataPermission(method,aClass);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.delegate=new InitializingDataPermissionResolverContainer(applicationContext);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值