前言

随着移动互联网的发展,前端开发领域也越来越广,前端早已经告别了切图的时代,迎来了规模化,工程化的大前端时代。近几年随着react、angular、vue等前端框架的兴起,前后端分离的架构迅速流行。但同时权限控制也带来了问题。

前后端分离之后,虽然前端也会进行权限控制、但是都比较简单。而且仅仅前端进行权限控制并不是真正意义的权限控制,用户完全可以绕开前端控制直接向后端发起请求。

权限设计

迄今为止最为普及的权限设计模型是RBAC模型,基于角色的访问控制(Role-Based Access Control)

前后端分离 springsecurity权限控制 前后端分离权限设计_shiro

市面上流行的Apache Shrio、Spring Security,都是基于此模型设计的。权限系统可以说是整个系统中最基础,同时也可以很复杂的,在实际项目中,会遇到多个系统,多个用户类型,多个使用场景,这就需要具体问题具体分析,但最核心的RBAC模型是不变的,我们可以在其基础上进行扩展来满足需求。

下面是简单的用户、角色、操作、机构数据库表设计,基本满足了大多数业务场景。

前后端分离 springsecurity权限控制 前后端分离权限设计_shiro_02

前端控制

用户登录成功会生成一个Token,其中会附带一些基本的用户信息,不建议附带角色权限信息。用户向后端发送请求都会附带这个Token

前端一般是菜单和按钮控制,在用户登录认证成功之后,根据用户ID实时获取菜单信息并渲染。按钮控制的话,情况比较复杂,如果要求不是很高可以一次性查询出来,放入本地缓存,进行本地鉴权。

这里撸一个比较简单的实现:

hasRole: function(roles){
  var roleNames = this.userInfo.roleNames;
  if(roleNames!=""){
      var role = roles.split(",");
      var array  = roleNames.split(",");
      for(var i=0;i<role.length;i++){
          if(array.indexOf(role[i])>-1){
             return true;
          }
      }
  }
  return false;
}

按钮控制:

<i-button type="primary" v-if="hasRole('admin')" icon="ios-cloud-download"  @click="exportCashReports">导出</i-button>

后端控制

前端虽然进行了控制,但这远远不够,特殊用户完全可以避开前端控制直接向后端发起请求,这时候如果后端没有对请求进行安全校验,很容易会把敏感数据暴露出去。

在单体架构时代,如果我使用了安全框架,我只需要一个注解,亦或者一个拦截配置就可以搞定。

比如这样:

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean (SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        Map<String, Filter> filtersMap = new LinkedHashMap<>();
        filtersMap.put("kickout", kickoutSessionControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        /**
         * 静态文件
         */
        filterChainDefinitionMap.put("/css/**","anon");
        filterChainDefinitionMap.put("/images/**","anon");
        filterChainDefinitionMap.put("/js/**","anon");
        filterChainDefinitionMap.put("/file/**","anon");
        /**
         * 登录
         */
        filterChainDefinitionMap.put("/login.html","anon");
        filterChainDefinitionMap.put("/sys/logout","anon");
        filterChainDefinitionMap.put("/sys/login","anon");
        /**
         * 管理后台
         */
        filterChainDefinitionMap.put("/sys/**", "roles[admin]");
        filterChainDefinitionMap.put("/**", "kickout,authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
}

或者这样:

/**
 * 角色列表
 */
 @PostMapping("/list")
 @RequiresRoles(value="admin")
 public Result list(SysRole role){
     return sysRoleService.list(role);
 }

前后端分离之后,后端并不会保存任何用户信息,只能通过前端传输的token进行校验,这里参考Shrio的实现思路,自己撸一个注解来实现权限校验。

/**
 * 权限注解
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {
 
    /**
     * A single String role name or multiple comma-delimitted role names required in order for the method
     * invocation to be allowed.
     */
    String[] value();
 
    /**
     * The logical operation for the permission check in case multiple roles are specified. AND is the default
     * @since 1.1.0
     */
    Logical logical() default Logical.OR;
}

然后写个拦截器对请求进行拦截,这里截取部分认证代码:

/**
 * @Description
 * @Author 小柒2012
 * @Date 2020/6/12
 */
@Component
public class AuthUtils {

    @Autowired
    private RedisUtils redisUtils;

    public boolean check(Object handler,String userId){
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Annotation permAnnotation= handlerMethod.getMethod().getAnnotation(RequiresPermissions.class);
        if(permAnnotation!=null) {
            String[] role = handlerMethod.getMethod().getAnnotation(RequiresPermissions.class).value();
            List<String> roleList = Arrays.asList(role);
            /**
             * 获取用户实际权限
             */
            List<Object> list = redisUtils.lGet("perms:"+userId,0,-1);
            List<String> permissions = roleList.stream().filter(item -> list.contains(item)).collect(toList());
            if (permissions.size() == 0) {
                return false;
            }
        }else{
            Annotation roleAnnotation= handlerMethod.getMethod().getAnnotation(RequiresRoles.class);
            if(roleAnnotation!=null){
                String[] role = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).value();
                List<String> roleList = Arrays.asList(role);
                /**
                 * 获取用户实际角色
                 */
                List<Object> list = redisUtils.lGet("roles:"+userId,0,-1);
                List<String> roles = roleList.stream().filter(item -> list.contains(item)).collect(toList());
                if (roles.size() == 0) {
                    return false;
                }
            }
        }
        return true;
    }
}

以上,在用户登录成功以后会保存用户的角色信息到缓存,生产环境中可以根据业务需求的实时性,在读取数据库和读取缓存上自行选择。

小结

前后端开发不是什么银弹,微服务也不是,不要盲目的选型跟风,也不要学习什么大厂经验,更不要谈什么技术债,适合自己团队的才是最好的。

当然,项目不高大上一些,出去怎么吹逼,怎么扩展团队规模,怎么升职加薪,所以我前面的都是废话,该分还是得分!!!

最后还是要推荐一下妹子图微服务版本,权限设计参考于此。

https://gitee.com/52itstyle/SPTools-Cloud

前后端分离 springsecurity权限控制 前后端分离权限设计_js_03