访问权限控制
RBAC
基于角色的访问控制(Role-Based Access Control)
是按角色进行授权,如主体的角色为总经理时才可以查询企业运营报表和员工工资信息等
- 缺点:查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为
判断用户角色是否为总经理或部门经理
,系统可扩展性差
基于资源的访问控制(Resource-Based Access Control)
是按资源/权限进行授权,把权限打包给角色(角色拥有一组权限)分配给用户(用户拥有多个角色)
- 优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强
权限数据模型
**基于资源的访问控制最少包括用户表、角色表、用户角色表、权限表、角色权限表
五张表 **
用户表
: 记录系统中的用户信息
角色表
: 根据系统业务决定系统中所需要的角色
菜单权限表
: 记录系统中操作相关资源的权限
用户角色表
: 指定用户和对应角色的关联关系,一个用户可以有多个角色,一个角色可以被不同用户使用
角色权限表
: 指定角色和权限的关联关系,一个角色可以拥有多个权限,一个权限也可以被多个角色使用
查询/删除/分配权限
查询用户所拥有的权限
: 先根据用户Id查询用户所拥有的角色Id,根据用户的角色Id查询用户所拥有的权限Id,根据权限Id查询具体的权限内容
# 一个用户可以拥有多个角色Id,一个角色Id可以有多个权限
SELECT * FROM xc_menu WHERE id IN(
SELECT menu_id FROM xc_permission WHERE role_id IN(
SELECT role_id FROM xc_user_role WHERE user_id = '49'
)
)
给用户的角色添加权限
- 首先根据用户的
Id
查询用户对应的角色,如果没有角色需要先给用户分配角色,有了角色后找到需要分配的权限主键ID
,在角色权限关系表
中添加一条记录指定用户角色对应的权限 - 分配完权限后需要重新登录刷新令牌中保存的权限信息
删除用户权限
的两种方式
第一种
: 给用户换角色(不含删除的权限),此时新角色下的权限就是用户的权限第二种
: 删除角色权限关系表
相应记录,此时拥有该角色的用户都将删除此权限
实现用户授权
开发中可以使用图形化的权限管理界面操
作数据库完成给用户分配权限、查询用户权限等需求
环境搭建
在nginx.conf
中进行配置代理教学机构的管理页面
upstream uidevserver{
server 127.0.0.1:8601 weight=10;
}
# 前端教学机构管理页面对应虚拟机
server {
listen 80;
server_name teacher.51xuecheng.cn;
// .................
location / {
proxy_pass http://uidevserver;
}
location /api/ {
proxy_pass http://gatewayserver/;
}
}
实现用户授权
使用Spring Security
进行授权,首先在生成jwt令牌前根据用户Id查询用户拥有的权限,然后将查询到的权限写入令牌
第一步: 在内容管理模块
集成Spring Security
,在需要授权的接口处使用@PreAuthorize("hasAuthority('权限标识符')")
示执行此方法需要授权
- 如果当前用户请求接口没有权限则抛出异常
org.springframework.security.access.AccessDeniedException
并提示不允许访问
@RestController
@Api(value = "课程信息编辑接口", tags = "课程信息编辑接口")
public class CourseBaseInfoController {
@Resource
CourseBaseInfoService courseBaseInfoService;
// 指定访问/course/list接口时需要拥有权限xc_teachmanager_course_list
@ApiOperation("课程查询接口")
@PreAuthorize("hasAuthority('xc_teachmanager_course_list')")
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
Long companyId = null;
if (StringUtils.isNotEmpty(user.getCompanyId())) {
companyId = Long.parseLong(user.getCompanyId());
}
PageResult<CourseBase> result = courseBaseInfoService.queryCourseBaseList(companyId, pageParams, queryCourseParams);
return result;
}
}
第二步: 在base工程下的统一异常处理器GlobalExceptionHandler
中处理该异常,为了避免在base工程引入Spring Security
依赖(工程也依赖了base工程,引入就会管控工程资源)
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 在系统异常中处理AccessDeniedException类型的异常
@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e) {
log.error("【系统异常】{}",e.getMessage(),e);
e.printStackTrace();
if(e.getMessage().equals("不允许访问")){
return new RestErrorResponse("没有操作此功能的权限");
}
return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());
}
}
第三步: 编写DAO方法根据用户ID查询用户的权限
public interface XcMenuMapper extends BaseMapper<XcMenu> {
@Select("SELECT * FROM xc_menu WHERE id IN (
SELECT menu_id FROM xc_permission WHERE role_id IN (
SELECT role_id FROM xc_user_role WHERE user_id = #{userId} ))
")
List<XcMenu> selectPermissionByUserId(@Param("userId") String userId);
}
第四步: 修改UserServiceImpl
类的getUserPrincipal
方法,当认证通过后从数据库中查询用户的权限信息并封装到UserDetails的Username属性
中,最后将信息写入到JWT令牌中
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 将包含认证参数的Json格式的字符串如username={"username":"yunqing","authType":"password","password":"111111"}`转换为AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
log.error("认证请求数据格式不对:{}", s);
throw new RuntimeException("认证请求数据格式不对");
}
// 获取认证类型,beanName由认证类型和后缀组成,如password_authservice
String authType = authParamsDto.getAuthType();
// 根据认证类型从Spring容器中取出对应的bean
AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class);
// 不同认证方式的认证逻辑不同,但最后都是调用execute方法完成认证
XcUserExt user = authService.execute(authParamsDto);
// 认证通过后,查询用户信息并将查询到的信息封装到UserDetails的Username属性中
return getUserPrincipal(user);
}
public UserDetails getUserPrincipal(XcUserExt user){
String password = user.getPassword();
// 默认权限,当用户权限为空时如果不设置权限则报Cannot pass a null GrantedAuthority collection
String[] authorities= {"test"};
// 根据用户id查询用户的所有权限
List<XcMenu> xcMenus = menuMapper.selectPermissionByUserId(user.getId());
if(xcMenus.size()>0){
List<String> permissions =new ArrayList<>();
xcMenus.forEach(m->{
// 获取用户拥有的权限标识符(菜单编码)并添加到集合里,如xc_teachmanager_course_add表示添加课程权限
permissions.add(m.getCode());
});
// 将permissions转成数组
authorities = permissions.toArray(new String[0]);
}
// 将用户权限放在XcUserExt中
user.setPermissions(permissions);
// 为了安全在令牌中不放密码
user.setPassword(null);
// 将user对象转json
String userString = JSON.toJSONString(user);
// 设置用户的权限
String[] authorities = permissions.toArray(new String[0]);
// 返回UserDetails包含查询到的用户权限信息,最终安全框架会把这些信息写入生成的JWT令牌中
UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
return userDetails;
}
第五步: 使用教学机构用户登录系统, 点击教学机构
首先访问课程查询接口,如果当前登陆用户没有xc_teachmanager_course_list
权限报没有此操作的权限
错误
细粒度授权(数据权限)
细粒度授权也叫数据范围授权,即不同的用户虽然所拥有的操作权限
相同,但是能够操作的数据范围
是不一样的
- 如用户A和用户B都拥有
查询课程
权限,但是查询课程时只允许用户查询自己所属的教学机构下的课程信息
细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户
进行校验,根据不同的参数
查询或操作不同的数据
- 获取当前登录的用户所属教育机构的Id,然后查询该教学机构下的课程信息
@ApiOperation("课程查询接口")
@PreAuthorize("hasAuthority('xc_teachmanager_course_list')")//拥有课程列表查询的权限方可访问
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParams){
// 使用工具类取出用户身份
XcUser user = SecurityUtil.getUser();
// 获取用户所属教育机构的Id
String companyId = user.getCompanyId();
return courseBaseInfoService.queryCourseBaseList(Long.parseLong(companyId),pageParams,queryCourseParams);
}
@Slf4j
public class SecurityUtil {
public static XcUser getUser() {
try {
// 通过SecurityContextHolder获取user_name属性的值即包含用户信息的Json字符串
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof String) {
// 将包含用户信息的Json字符串转换为XcUser对象
String userJson = principal.toString();
XcUser xcUser = JSON.parseObject(userJson, XcUser.class);
return xcUser;
}
} catch (Exception e) {
log.error("获取当前登录用户身份信息出错:{}", e.getMessage());
e.printStackTrace();
}
return null;
}
// 这里使用内部类是为了不让content工程去依赖auth工程
@Data
public static class XcUser implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String username;
private String password;
private String salt;
private String name;
private String nickname;
private String wxUnionid;
private String companyId;
/**
* 头像
*/
private String userpic;
private String utype;
private LocalDateTime birthday;
private String sex;
private String email;
private String cellphone;
private String qq;
/**
* 用户状态
*/
private String status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
}
@Override
@Transactional
public PageResult<CourseBase> queryCourseBaseList(Long companyId, PageParams pageParams, QueryCourseParamDto queryCourseParams) {
// 构建条件查询器
LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
// 构建查询条件,按照机构ID查询
queryWrapper.eq(CourseBase::getCompanyId, companyId);
// 构建查询条件:按照课程名称模糊查询
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParams.getCourseName()), CourseBase::getName, queryCourseParams.getCourseName());
// 构建查询条件,按照课程审核状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getAuditStatus()), CourseBase::getAuditStatus, queryCourseParams.getAuditStatus());
// 构建查询条件,按照课程发布状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getPublishStatus()), CourseBase::getStatus, queryCourseParams.getPublishStatus());
// 分页对象
Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<CourseBase> pageInfo = courseBaseMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<CourseBase> items = pageInfo.getRecords();
// 获取数据总条数
long counts = pageInfo.getTotal();
// 构建结果集
return new PageResult<>(items, counts, pageParams.getPageNo(), pageParams.getPageSize());
}