赞
踩
最近在开始写【spring源码深度解析】系列文章,如果有对spring源码感兴趣的同学,欢迎点击左上角我的头像,查看相关文章,该系列文章均是结合我当时学习源码的经历编写完成,不是一上来就堆砌源码,而是从日常使用展开学习,力求让初学者弄懂spring相关模块的实现
首先提出一个问题
:在后台管理系统中,我们一般会根据当前登录用户查出直属相关的数据做列表展示,列表中有一列都会供用户点击来查看详情或其他信息,比如我是个保险公司的销售员,我要看我签约了哪些客户,然后点击某一个客户的详情看看签的合同细节,大家一般会怎么做?
有些同学可能会这么实现(采用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.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,标注在类或方法上,对符合条件的方法进行拦截,若处理器校验通过,则调用链继续,否则抛出特定异常。
由于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接口相信大家应该知道怎么弄了吧!
下面结合流程图具体介绍下框架的逻辑
重点说下第三步:
@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;
}
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); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。