垂直越权是一种非常常见且非常严重的权限漏洞,具体表现就是,低权限的用户可以不受控制的访问高权限用户的资源。
其实业界有现成的权限框架可以解决这个问题,比如Shiro、SpringSecurity,但是框架一般都比较重,如果我们的系统对权限校验的要求比较简单,那么就可以考虑自己来实现一套防止垂直越权的体系。
在开始如下方案的介绍前,需要拥有Spring拦截器、自定义注解、JavaConfig的相关知识。
方案一、
基于资源限定的角色来进行垂直越权控制
我们先构建一个基础的RestDemo应用,采用Spring Boot构建,然后新建一个RestController类如下:
@RestController
public class AuthTest {
@RolePermitted(roleList = {"admin","leader"})
@GetMapping(value = "/getMoney")
public String getMoney(){
return "1000";
}
@RolePermitted(roleList = {"admin","leader","employee"})
@GetMapping(value = "/storeMoney/{num}")
public String storeMoney(@PathVariable("num") String num){
return num + " money stored";
}
}
然后定义一个自定义注解@RolePermitted,用来表示哪些角色可以访问当前这个资源。
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RolePermitted {
String[] roleList();
}
以上,我们已经完成了基本内容的开发,剩下的就是创建一个拦截器并进行配置的注册了。
@Slf4j
public class RoleBasedAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
// 对标注了RolePermitted注解的方法访问进行权限校验
if(method.isAnnotationPresent(RolePermitted.class)){
checkRolePermission(method);
}
// 只有当权限校验通过,不抛出异常的时候才能通过
return true;
}
private void checkRolePermission(Method method) throws Exception {
// 获取当前方法允许访问的角色列表
String[] roleList = method.getAnnotation(RolePermitted.class).roleList();
// 模拟从数据库/redis/缓存中获取用户的实际角色是employee
String currentUserRole = "employee";
for (String role : roleList) {
if(role.equals(currentUserRole)){
log.info("{}权限校验通过, 允许访问的角色列表是{},当前用户角色是{}", method.getName(), roleList ,currentUserRole);
return;
}
}
log.warn("{}权限校验不通过, 允许访问的角色列表是{},当前用户角色是{}", method.getName(), roleList ,currentUserRole);
throw new Exception("权限校验不通过");
}
}
@Configuration
@EnableWebMvc
public class AuthConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RoleBasedAuthInterceptor());
}
}
此时,我们启动应用后,对目前仅有的全部两个URL的访问就都会走我们的RoleBasedAuthInterceptor拦截器进行角色的垂直越权校验,因为此时我们模拟的登录用户角色是employee,所以当访问/storeMoney/{num}
时没有问题,但是访问/getMoney
时就抛出了异常。
这种方案的优点是,实现起来非常简单,便于后续的维护,我们只需要对新增的URL添加对应的允许访问角色即可。但是缺点也非常的明显:
- 对于角色很多的系统,我们不得不在注解上加上很多允许访问的角色,会显得很冗长;
- 如果后续需要新增一个角色,那么就需要找到所有该角色允许访问的URL,挨个增加角色信息,费时费力,还容易遗漏;
- 万一在系统运行期间需要灵活调配不同角色允许访问的URL,几乎是不可能的,需要重新修改代码部署;
方案二、
基于资源和角色的配置关系来进行垂直越权控制
@RestController
public class AuthTest {
@ResourceCode(resourceCode = "getMoney")
@GetMapping(value = "/getMoney")
public String getMoney() {
return "1000";
}
@ResourceCode(resourceCode = "storeMoney")
@GetMapping(value = "/storeMoney/{num}")
public String storeMoney(@PathVariable("num") String num) {
return num + " money stored";
}
}
@Slf4j
public class ResourceCodeBasedAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
// 对标注了ResourceCode注解的方法访问进行权限校验
if(method.isAnnotationPresent(ResourceCode.class)){
checkRolePermission(method);
}
// 只有当权限校验通过,不抛出异常的时候才能通过
return true;
}
private void checkRolePermission(Method method) throws Exception {
// 获取当前方法的资源Code
String resourceCode = method.getAnnotation(ResourceCode.class).resourceCode();
// 模拟从数据库/redis/缓存中获取用户的可访问资源code列表
List<String> allowedResourceCodeList = new ArrayList<>();
allowedResourceCodeList.add("storeMoney");
allowedResourceCodeList.add("transferMoney");
if(allowedResourceCodeList.contains(resourceCode)){
log.info("{}权限校验通过, 当前用户的资源列表为{},当前资源的code为{}", method.getName(), allowedResourceCodeList ,resourceCode);
return;
}
log.warn("{}权限校验不通过, 当前用户的资源列表为{},当前资源的code为{}", method.getName(), allowedResourceCodeList ,resourceCode);
throw new Exception("权限校验不通过");
}
}
@Configuration
@EnableWebMvc
public class AuthConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ResourceCodeBasedAuthInterceptor());
}
}
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceCode {
String resourceCode();
}
在这个例子中,我们每个URL的Rest方法定义一个resourceCode来唯一标识它,然后在拦截器中,先加载出当前登录者所允许的全部resourceCode,只要当前方法的resourceCode被包含在所允许的全部resourceCode中,那么就证明当前用户可以访问。
至于如何获取当前登录者的所有resourceCode,我们可以通过如下的表设计来完成:
- auth_user,存储所有用户信息
- user_id
- user_name
- auth_role,存储所有角色信息
- role_id
- role_name
- auth_user_role_relation,存储用户和角色的关系,一个用户可以拥有多个角色(多条记录)
- relation_id
- uder_id
- role_id
- auth_resource_code,存储资源信息
- resource_id
- resource_code
- auth_role_resource_code_relation,存储角色和资源之间的关系,一个角色可以拥有多个资源(多条记录)
- relation_id
- role_id
- resource_id
我们只要执行如下的SQL语句就能得到当前登录者所有的resourceCode:
select rc.resource_code
from auth_user u
left join auth_user_role_relation urr on u.user_id = urr.user_id
left join auth_role_resource_code_relation rrcr on urr.role_id = rrcr.role_id
left join auth_resource_code rc on rrcr.resource_id = rc.resource_id
where u.user_name = ''
这种方案的优势是:
- 对于角色很多的系统,我们不需要在代码中标注;
- 如果后续需要新增一个角色,那么只需要在auth_role、auth_user_role_relation、auth_role_resource_code_relation中添加记录即可,不用去修改代码,改完立马就能生效;
- 可以很方便地在系统运行期间调配不同人员、不同角色的权限信息。
网友评论