基于RESTful API 设计用户权限控制
RESTful简述
本文是基于RESTful描述的,需要你对这个有初步的了解。 RESTful是什么? Representational State Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。 REST比较重要的点是资源和状态转换, 所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。 而 “状态转换”,则是把对应的HTTP协议里面,四个表示操作方式的动词分别对应四种基本操作:
- GET,用来浏览(browse)资源
- POST,用来新建(create)资源
- PUT,用来更新(update)资源
- DELETE,用来删除(delete)资源
-
资源的分类及操作
清楚了资源的概念,然后再来对资源进行一下分类,我把资源分为下面三类:
- 私人资源 (Personal Source)
- 角色资源 (Roles Source)
- 公共资源 (Public Source)
-
“私人资源”:是属于某一个用户所有的资源,只有用户本人才能操作,其他用户不能操作。例如用户的个人信息、订单、收货地址等等。 “角色资源”:与私人资源不同,角色资源范畴更大,一个角色可以对应多个人,也就是一群人。如果给某角色分配了权限,那么只有身为该角色的用户才能拥有这些权限。例如系统资源只能够管理员操作,一般用户不能操作。 “公共资源”:所有人无论角色都能够访问并操作的资源。
而对资源的操作,无非就是分为四种:
- 浏览 (browse)
- 新增 (create)
- 更新 (update)
- 删除 (delete)
角色、用户、权限之间的关系
角色和用户的概念,自不用多说,大家都懂,但是权限的概念需要提一提。
“权限”,就是资源与操作的一套组合,例如"增加用户"是一种权限,"删除用户"是一种权限,所以对于一种资源所对应的权限有且只有四种。 - 角色与用户的关系:一个角色对应一群用户,一个用户也可以扮演多个角色,所以它们是多对多的关系。
角色与权限的关系:一个角色拥有一堆权限,一个权限却只能属于一个角色,所以它们是一(角色)对多(权限)的关系
权限与用户的关系:由于一个用户可以扮演多个角色,一个角色拥有多个权限,所以用户与权限是间接的多对多关系。 需要注意两种特别情况:
- 私人资源与用户的关系,一种私人资源对应的四种权限只能属于一个用户,所以这种情况下,用户和权限是一(用户)对多(权限)的关系。
- 超级管理员的角色,这个角色是神一般的存在,能无视一切阻碍,对所有资源拥有绝对权限,甭管你是私人资源还是角色资源。
数据库表的设计
角色、用户、权限的模型应该怎么样设计,才能满足它们之间的关系?
对上图的一些关键字段进行说明:
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的运行顺序
- SessionAuthPolicy:
检测用户是否已经登录,用户登录是进行下面检测的前提。 - SourcePolicy:
检测访问的资源是否存在,主要检测Source表的记录 - PermissionPolicy:
检测该用户所属的角色,是否有对所访问资源进行对应操作的权限。 - OwnerPolicy:
如果所访问的资源属于私人资源,则检测当前用户是否该资源的拥有者。
如果通过所有policy的检测,则把请求转发到目标action。
Sails下的权限控制实现
在Sails下,有一个很方便的套件sails-permissions,集成了一套权限管理的方案,本文也是基于该套件的源码所引出来的权限管理解决方案。
结语
对程序员最大的挑战,并不是能否掌握了哪些编程语言,哪些软件框架,而是对业务和需求的理解,然后在此基础上,把要点抽象出来,写成计算机能理解的语言。
最后,希望这篇文章,能够帮助你对权限管理这一课题增加多一点点理解。- 附上thinkphp restful类库 帮助理解 验证方式什么的这里面没有 需要自己写
-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
<?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
)));
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 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】