基于RESTful API 设计用户权限控制

RESTful简述

本文是基于RESTful描述的,需要你对这个有初步的了解。 RESTful是什么? Representational State Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。 REST比较重要的点是资源状态转换, 所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。 而 “状态转换”,则是把对应的HTTP协议里面,四个表示操作方式的动词分别对应四种基本操作:

  1. GET,用来浏览(browse)资源
  2. POST,用来新建(create)资源
  3. PUT,用来更新(update)资源
  4. DELETE,用来删除(delete)资源
  • 资源的分类及操作

    清楚了资源的概念,然后再来对资源进行一下分类,我把资源分为下面三类:

    1. 私人资源 (Personal Source)
    2. 角色资源 (Roles Source)
    3. 公共资源 (Public Source)
    • “私人资源”:是属于某一个用户所有的资源,只有用户本人才能操作,其他用户不能操作。例如用户的个人信息、订单、收货地址等等。 “角色资源”:与私人资源不同,角色资源范畴更大,一个角色可以对应多个人,也就是一群人。如果给某角色分配了权限,那么只有身为该角色的用户才能拥有这些权限。例如系统资源只能够管理员操作,一般用户不能操作。 “公共资源”:所有人无论角色都能够访问并操作的资源。

      而对资源的操作,无非就是分为四种:

      1. 浏览 (browse)
      2. 新增 (create)
      3. 更新 (update)
      4. 删除 (delete)

      角色、用户、权限之间的关系

      角色和用户的概念,自不用多说,大家都懂,但是权限的概念需要提一提。
      “权限”,就是资源与操作的一套组合,例如"增加用户"是一种权限,"删除用户"是一种权限,所以对于一种资源所对应的权限有且只有四种。

    • 角色用户的关系:一个角色对应一群用户,一个用户也可以扮演多个角色,所以它们是多对多的关系。
      角色权限的关系:一个角色拥有一堆权限,一个权限却只能属于一个角色,所以它们是一(角色)对多(权限)的关系
      权限用户的关系:由于一个用户可以扮演多个角色,一个角色拥有多个权限,所以用户与权限是间接的多对多关系。
    • 需要注意两种特别情况:

      1. 私人资源与用户的关系,一种私人资源对应的四种权限只能属于一个用户,所以这种情况下,用户和权限是一(用户)对多(权限)的关系。
      2. 超级管理员的角色,这个角色是神一般的存在,能无视一切阻碍,对所有资源拥有绝对权限,甭管你是私人资源还是角色资源。

      数据库表的设计

      角色、用户、权限的模型应该怎么样设计,才能满足它们之间的关系?

    • 对上图的一些关键字段进行说明:

      Source
      • name: 资源的名称,也就是其他模型的名称,例如:user、role等等。
      • identity: 资源的唯一标识,可以像uuid,shortid这些字符串,也可以是model的名称。
      • permissions : 一种资源对应有四种权限,分别对这种资源的browse、create、update、delete
      Permission
      • source : 该权限对应的资源,也就是Source的某一条记录的唯一标识
      • action :对应资源的操作,只能是browse、create、update、delete四个之一
      • relation:用来标记该权限是属于私人的,还是角色的,用于OwnerPolicy检测
      • roles: 拥有该权限的角色
      Role
      • users : 角色所对应的用户群,一个角色可以对应多个用户
      • permissions: 权限列表,一个角色拥有多项权利
      User
      • createBy : 该记录的拥有者,在user标里,一般等于该记录的唯一标识,这一属性用于OwnerPolicy的检测,其他私有资源的模型设计,也需要加上这一字段来标识资源的拥有者。
      • roles : 用户所拥有的角色

      策略/过滤器

      在sails下称为策略(Policy),在java SSH下称为过滤器(Filter),无论名称如何,他们工作原理是大同小异的,主要是在一条HTTP请求访问一个Controller下的action之前进行检测。所以在这一层,我们可以自定义一些策略/过滤器来实现权限控制。
      为行文方便,下面姑且允许我使用策略这一词。

      ** 策略 (Policy) **

      下面排版顺序对应Policy的运行顺序

      1. SessionAuthPolicy
        检测用户是否已经登录,用户登录是进行下面检测的前提。
      2. SourcePolicy
        检测访问的资源是否存在,主要检测Source表的记录
      3. PermissionPolicy
        检测该用户所属的角色,是否有对所访问资源进行对应操作的权限。
      4. OwnerPolicy
        如果所访问的资源属于私人资源,则检测当前用户是否该资源的拥有者。

      如果通过所有policy的检测,则把请求转发到目标action。

    Sails下的权限控制实现

    在Sails下,有一个很方便的套件sails-permissions,集成了一套权限管理的方案,本文也是基于该套件的源码所引出来的权限管理解决方案。

    结语

    对程序员最大的挑战,并不是能否掌握了哪些编程语言,哪些软件框架,而是对业务和需求的理解,然后在此基础上,把要点抽象出来,写成计算机能理解的语言。
    最后,希望这篇文章,能够帮助你对权限管理这一课题增加多一点点理解。

    • 附上thinkphp restful类库 帮助理解 验证方式什么的这里面没有 需要自己写
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      <?php
      // +----------------------------------------------------------------------
      // | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
      // +----------------------------------------------------------------------
      // | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
      // +----------------------------------------------------------------------
      // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
      // +----------------------------------------------------------------------
      // | Author: liu21st <liu21st@gmail.com>
      // +----------------------------------------------------------------------
      namespace Think\Controller;
      use Think\Controller;
      /**
       * ThinkPHP REST控制器类
       */
      class RestController extends Controller {
          // 当前请求类型
          protected   $_method        =   '';
          // 当前请求的资源类型
          protected   $_type          =   '';
          // REST允许的请求类型列表
          protected   $allowMethod    =   array('get','post','put','delete');
          // REST默认请求类型
          protected   $defaultMethod  =   'get';
          // REST允许请求的资源类型列表
          protected   $allowType      =   array('html','xml','json','rss');
          // 默认的资源类型
          protected   $defaultType    =   'html';
          // REST允许输出的资源类型列表
          protected   $allowOutputType=   array
                          'xml' => 'application/xml',
                          'json' => 'application/json',
                          'html' => 'text/html',
                      );
       
         /**
           * 架构函数
           * @access public
           */
          public function __construct() {
              // 资源类型检测
              if(''==__EXT__) { // 自动检测资源类型
                  $this->_type   =  $this->getAcceptType();
              }elseif(!in_array(__EXT__,$this->allowType)) {
                  // 资源类型非法 则用默认资源类型访问
                  $this->_type   =  $this->defaultType;
              }else{
                  // 检测实际资源类型
                  $this->_type   =  $this->getAcceptType() == __EXT__ ? __EXT__ : $this->defaultType;
              }
       
              // 请求方式检测
              $method  strtolower(REQUEST_METHOD);
              if(!in_array($method,$this->allowMethod)) {
                  // 请求方式非法 则用默认请求方法
                  $method = $this->defaultMethod;
              }
              $this->_method = $method;
               
              parent::__construct();
          }
       
          /**
           * 魔术方法 有不存在的操作的时候执行
           * @access public
           * @param string $method 方法名
           * @param array $args 参数
           * @return mixed
           */
          public function __call($method,$args) {
              if( 0 === strcasecmp($method,ACTION_NAME.C('ACTION_SUFFIX'))) {
                  if(method_exists($this,$method.'_'.$this->_method.'_'.$this->_type)) { // RESTFul方法支持
                      $fun  $method.'_'.$this->_method.'_'.$this->_type;
                      $this->$fun();
                  }elseif($this->_method == $this->defaultMethod && method_exists($this,$method.'_'.$this->_type) ){
                      $fun  $method.'_'.$this->_type;
                      $this->$fun();
                  }elseif($this->_type == $this->defaultType && method_exists($this,$method.'_'.$this->_method) ){
                      $fun  $method.'_'.$this->_method;
                      $this->$fun();
                  }elseif(method_exists($this,'_empty')) {
                      // 如果定义了_empty操作 则调用
                      $this->_empty($method,$args);
                  }elseif(file_exists_case($this->view->parseTemplate())){
                      // 检查是否存在默认模版 如果有直接输出模版
                      $this->display();
                  }else{
                      E(L('_ERROR_ACTION_').':'.ACTION_NAME);
                  }
              }
          }
       
          /**
           * 获取当前请求的Accept头信息
           * @return string
           */
          protected function getAcceptType(){
              $type = array(
                  'html'  =>  'text/html,application/xhtml+xml,*/*',
                  'xml'   =>  'application/xml,text/xml,application/x-xml',
                  'json'  =>  'application/json,text/x-json,application/jsonrequest,text/json',
                  'js'    =>  'text/javascript,application/javascript,application/x-javascript',
                  'css'   =>  'text/css',
                  'rss'   =>  'application/rss+xml',
                  'yaml'  =>  'application/x-yaml,text/yaml',
                  'atom'  =>  'application/atom+xml',
                  'pdf'   =>  'application/pdf',
                  'text'  =>  'text/plain',
                  'png'   =>  'image/png',
                  'jpg'   =>  'image/jpg,image/jpeg,image/pjpeg',
                  'gif'   =>  'image/gif',
                  'csv'   =>  'text/csv'
              );
               
              foreach($type as $key=>$val){
                  $array   explode(',',$val);
                  foreach($array as $k=>$v){
                      if(stristr($_SERVER['HTTP_ACCEPT'], $v)) {
                          return $key;
                      }
                  }
              }
              return false;
          }
       
          // 发送Http状态信息
          protected function sendHttpStatus($code) {
              static $_status = array(
                  // Informational 1xx
                  100 => 'Continue',
                  101 => 'Switching Protocols',
                  // Success 2xx
                  200 => 'OK',
                  201 => 'Created',
                  202 => 'Accepted',
                  203 => 'Non-Authoritative Information',
                  204 => 'No Content',
                  205 => 'Reset Content',
                  206 => 'Partial Content',
                  // Redirection 3xx
                  300 => 'Multiple Choices',
                  301 => 'Moved Permanently',
                  302 => 'Moved Temporarily '// 1.1
                  303 => 'See Other',
                  304 => 'Not Modified',
                  305 => 'Use Proxy',
                  // 306 is deprecated but reserved
                  307 => 'Temporary Redirect',
                  // Client Error 4xx
                  400 => 'Bad Request',
                  401 => 'Unauthorized',
                  402 => 'Payment Required',
                  403 => 'Forbidden',
                  404 => 'Not Found',
                  405 => 'Method Not Allowed',
                  406 => 'Not Acceptable',
                  407 => 'Proxy Authentication Required',
                  408 => 'Request Timeout',
                  409 => 'Conflict',
                  410 => 'Gone',
                  411 => 'Length Required',
                  412 => 'Precondition Failed',
                  413 => 'Request Entity Too Large',
                  414 => 'Request-URI Too Long',
                  415 => 'Unsupported Media Type',
                  416 => 'Requested Range Not Satisfiable',
                  417 => 'Expectation Failed',
                  // Server Error 5xx
                  500 => 'Internal Server Error',
                  501 => 'Not Implemented',
                  502 => 'Bad Gateway',
                  503 => 'Service Unavailable',
                  504 => 'Gateway Timeout',
                  505 => 'HTTP Version Not Supported',
                  509 => 'Bandwidth Limit Exceeded'
              );
              if(isset($_status[$code])) {
                  header('HTTP/1.1 '.$code.' '.$_status[$code]);
                  // 确保FastCGI模式下正常
                  header('Status:'.$code.' '.$_status[$code]);
              }
          }
       
          /**
           * 编码数据
           * @access protected
           * @param mixed $data 要返回的数据
           * @param String $type 返回类型 JSON XML
           * @return string
           */
          protected function encodeData($data,$type='') {
              if(empty($data))  return '';
              if('json' == $type) {
                  // 返回JSON数据格式到客户端 包含状态信息
                  $data = json_encode($data);
              }elseif('xml' == $type){
                  // 返回xml格式数据
                  $data = xml_encode($data);
              }elseif('php'==$type){
                  $data = serialize($data);
              }// 默认直接输出
              $this->setContentType($type);
              //header('Content-Length: ' . strlen($data));
              return $data;
          }
       
          /**
           * 设置页面输出的CONTENT_TYPE和编码
           * @access public
           * @param string $type content_type 类型对应的扩展名
           * @param string $charset 页面输出编码
           * @return void
           */
          public function setContentType($type, $charset=''){
              if(headers_sent()) return;
              if(empty($charset))  $charset = C('DEFAULT_CHARSET');
              $type = strtolower($type);
              if(isset($this->allowOutputType[$type])) //过滤content_type
                  header('Content-Type: '.$this->allowOutputType[$type].'; charset='.$charset);
          }
       
          /**
           * 输出返回数据
           * @access protected
           * @param mixed $data 要返回的数据
           * @param String $type 返回类型 JSON XML
           * @param integer $code HTTP状态
           * @return void
           */
          protected function response($data,$type='',$code=200) {
              $this->sendHttpStatus($code);
              exit($this->encodeData($data,strtolower($type)));
          }
      }

       

posted @   左闯  阅读(5278)  评论(1编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示