开始写之前:最近几个月一直在研究eladmin框架,在eladmin基础上实现自己的系统,由于这个开源框架的易用性和高效率,导致我并没有理解透彻框架本身,只是照葫芦画瓢的去做,几个月做下来学到的知识微乎其微。以前我也没有记录的习惯,即使遇到一个BUG,百度完了解决了,下次遇到同样的BUG还是同样是去百度解决,基于此,为了养成一个好的学习习惯,以后我要将学到的知识,遇到的问题,都记录于此,以供自己复盘和积累经验。
新的一天开始啦———————————————————————————————————
eladmin系统权限控制采用的是RBAC思想,由此构成“用户—权限—菜单”的授权模型,在这种模型中,用户与权限,权限与菜单之间构成了多对多的关系。eladmin系统采用的安全框架是Spring Security + Jwt Token,访问后端接口需在请求头中携带token进行访问,请求头格式如下:
1.Spring Security是什么?
Spring Security是一个专注于为Java应用程序提供身份验证和授权、访问控制的框架。
原理:创建大量的过滤器filter和拦截器interceptor来进行请求的验证和拦截,以此达到安全。
2.JWT(Json Web Tokens)是什么?
JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。
JWT的基本原理,基本流程如下:
(1)客户端使用账号和密码请求登录接口;
(2)登录成功后服务器使用签名密钥生成JWT,然后返回JWT给客户端;
(3)客户端再次像服务端请求其他接口时会带上JWT;
(4)服务器收到JWT后验证签名的有效性,对客户端做出相应的响应。
JWT的基本数据结构:它是由 Header(头部), PayLoad(负载),Signature(签名)组成的。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc1MzczNX0.h1XmQo017udxlFsH-8US9Lg8dJ0IDsSbRbjEN5Nq0l4
3. 数据交互流程如下:
用户登录 -> 后端验证登录返回 token
-> 前端带上token
请求后端数据 -> 后端返回数据。
4.权限注解:
eladmin框架中Spring Security提供了Spring EL表达式,允许我们在定义接口访问的方法上面添加注解,来控制访问权限。下面的接口表示用户拥有admin、menu:edit权限中的任意一个就能能访问update方法, 如果方法不加@preAuthorize注解,意味着所有用户都需要带上有效的token后能访问update方法。
@Log(description = "修改菜单")
@PutMapping(value = "/menus")
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
// 略
}
由于每个接口都需要给超级管理员放行,而使用hasAnyRole('admin','user:list')每次都需要重复的添加 admin 权限,因此在新版本 (2.3) 中加入了自定义权限验证方式,在验证的时候默认给拥有admin权限的用户放行。
/**
* 自定义权限验证方式,在验证的时候默认给拥有admin权限的用户放行
* @author Zheng Jie
*/
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String ...permissions){
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtils.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
使用方式:
@PreAuthorize("@el.check('user:list','user:add')")
接口放行:
有些接口是不需要验证权限的,这个时候就需要我们给接口放行,使用方式如下:
(1)使用注解方式:只需要在Controller的方法上加入该注解即可
@AnonymousAccess
分割线————————————————————————————————————————
当我在页面想自己实现权限控制的时候,SecurityUtils是一个很有用的类,这个类中定义了获取当前登录的用户的相关信息可以供我们在查询时添加条件以此实现对数据显示的权限控制,因为目前框架实现的是对菜单、增删改查的控制,所以当我们想对前端显示的数据根据不同角色进行控制时,就可以在前端通过JPA的criteria查询增加条件实现,但需要注意的是,比如同时有两个条件时,查询的是两个条件的并集,如果查询两个条件的交集,还需要在serviceImpl中自定义实现:
controller层先将criteria进行赋值,service层查询时就会查=该条件的值
并集:
@GetMapping(value = "/user")
@ApiOperation("用户日志查询")
public ResponseEntity<Object> queryUserLog(LogQueryCriteria criteria, Pageable pageable){
criteria.setLogType("INFO");
criteria.setBlurry(SecurityUtils.getCurrentUsername());
return new ResponseEntity<>(logService.queryAllByUser(criteria,pageable), HttpStatus.OK);
}
@Override
public Object queryAllByUser(LogQueryCriteria criteria, Pageable pageable) {
Page<Log> page = logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)), pageable);
return PageUtil.toPage(page.map(logSmallMapper::toDto));
}
交集:
@Override
public Map<String, Object> findByCurrentUser(AllResourcesDto query, Pageable pageable) {
Sort sort = Sort.by(Sort.Direction.DESC, "id");
List<Requirement> requirements = requirementRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder),sort);
List<RequirementDto> requirement = new ArrayList<>();
String username = criteria.getCreateBy();
String deptname = criteria.getDeptName();
for (int i=0; i<requirements.size(); i++){
if (requirements.get(i).getCreateBy().equals(username) || requirements.get(i).getDept().getName().equals(deptname) ){
requirement.add(requirementMapper.toDto(requirements.get(i)));
Long requirementId = requirements.get(i).getId();
List<String> projectName = requirementRepository.findProjectName(requirementId);
String projectNames = "";
for (int j=0; j<projectName.size(); j++){
projectNames += projectName.get(j)+".";
}
requirement.get(requirement.size()-1).setProjectNames(projectNames);
}
}
return PageUtil.toPage(
PageUtil.toPage(pageable.getPageNumber(),pageable.getPageSize(), requirement),
requirement.size()
);
}
当我们想根据当前用户的等级控制前端可编辑字段时,我们可以在后端获取当前用户的等级并传递给前端,前端通过函数取得当前用户的等级后,在form表单中根据等级数据进行可编辑控制:
@Override
public int getPower() {
List<Integer> levels = roleService.findByUsersId(SecurityUtils.getCurrentUserId()).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList());
int min = Collections.max(levels);
return min;
}
@ApiOperation("获取权限值")
@AnonymousPostMapping("/getPowers")
@AnonymousAccess
public ResponseEntity<Object> getPowers(){
return new ResponseEntity<>(requirementService.getPower(), HttpStatus.OK);
}
getPowers() {
let form = new FormData();
axios({
method: "post",
url: "/api/requirements/getPowers",
headers: {
"Content-type": "multipart/form-data",
Authorization: getToken(),
},
data: form,
}).then(
(res) => {
this.power = res.data
},
(err) => {
}
);
},
[CRUD.HOOK.beforeRefresh](crud, form) {
this.getPowers()
},
<el-form-item label="备注" width="125" prop="comments">
<el-input type="textarea" autosize v-model="form.comments" :disabled="power>2 && grant === 0"/>
</el-form-item>