首先提出一个问题
:在后台管理系统中,我们一般会根据当前登录用户查出直属相关的数据做列表展示,列表中有一列都会供用户点击来查看详情或其他信息,比如我是个保险公司的销售员,我要看我签约了哪些客户,然后点击某一个客户的详情看看签的合同细节,大家一般会怎么做?
有些同学可能会这么实现(采用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: ''}
]]
});
});
function showContractInfo(customerId) {
var htmls=' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'';
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);
}
}