# 开发文档

# 项目结构

LinCMS-TP5的目录结构遵循了ThinkPHP官方原生的tp5.1结构,如果你使用过TP5.0后者TP5.1那么对这个目录结构会很熟悉。
如果你没接触过那也不要紧,可以参考下面的示例或者查看ThinkPHP5.1完全开发手册了解更多

www  WEB部署目录(或者子目录)
├─application           应用目录
│  ├─common             公共模块目录(可以更改)
│  ├─api                模块目录
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  │  ├─cms          开发CMS API目录
│  │  │  └─v1           开发普通API目录
│  │  ├─model           模型目录
│  │  ├─validate        验证器目录
│  │  ├─config          配置目录
│  │  └─ ...            更多类库目录
│  │
│  ├─command.php        命令行定义文件
│  ├─common.php         公共函数文件
│  └─tags.php           应用行为扩展定义文件
│
├─config                应用配置目录
│  ├─module_name        模块配置目录
│  │  ├─database.php    数据库配置
│  │  ├─cache           缓存配置
│  │  └─ ...
│  │
│  ├─app.php            应用配置
│  ├─cache.php          缓存配置
│  ├─cookie.php         Cookie配置
│  ├─database.php       数据库配置
│  ├─log.php            日志配置
│  ├─session.php        Session配置
│  ├─template.php       模板引擎配置
│  └─trace.php          Trace配置
│
├─route                 路由定义目录
│  ├─route.php          路由定义
│  └─...                更多
│
├─public                WEB目录(对外访问目录)
│  ├─index.php          入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于apache的重写
│
├─thinkphp              框架系统目录
│  ├─lang               语言文件目录
│  ├─library            框架类库目录
│  │  ├─think           Think类库包目录
│  │  └─traits          系统Trait目录
│  │
│  ├─tpl                系统模板目录
│  ├─base.php           基础定义文件
│  ├─convention.php     框架惯例配置文件
│  ├─helper.php         助手函数文件
│  └─logo.png           框架LOGO文件
│
├─extend                扩展类库目录
├─runtime               应用的运行时目录(可写,可定制)
├─vendor                第三方类库目录(Composer依赖库)
├─build.php             自动生成定义文件(参考)
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件
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

上面是项目的整体结构,开发时我们强烈建议你遵循如下规范开发,在前期你肯 定会不适应,但慢慢地你会爱上它。

  • application/api/controller文件夹中开发API,并将不同版本,不同类型的API分开,如:v1 代表 第一版本的 API,v2 代表第二版本,cms 代表属于 cms 的 API。
  • 将程序的配置文件放在config文件夹下,并着重区分secure(安全性配置)setting(普通性配置)。配置更详细内容参考配置
  • 将可重用的类库放在application/lib文件夹下。
  • 将数据模型放在application/api/model文件夹下。
  • 将校验类放在application/api/validate文件夹下。

# API规范

我们在application/api/controller文件夹中开发不同版本(类型)的API,例如项目源码中,我们在application/api/controller文件夹中有两个子目录cmsv1 image

我们在对应版本(类型)的目录下去开发我们的控制器类,这样有利于我们管理和维护,说白了就是可以快速定位代码所在。

controller下的目录结构不是唯一也没有标准,项目源码仅提供一个参考。

接下来我们尝试通过阅读User控制器类的源码来了解下设计思路,其余的控制器类基本大同小异,以其中login方法为例

image

User类下面,我们定义了一个login方法,它就是一个Api,既然是Api,就必须要有一个与之对应的访问url,前端在访问这个url之后,就会路由到我们这个login方法。更多关于路由的介绍请阅读路由部分的内容。

这里方法里面做了如下几件事:

  1. 调用了一个自定义的验证器

  2. 获取了请求参数。

  3. 验证用户是否存在和密码是否正确。

  4. 生成令牌

  5. 记录日志

  6. 返回令牌

这里的代码很简单,控制器层没有做任何的逻辑判断和业务实现,交给了对应的模型或者方法去执行。控制器只负责流程的控制而不关心具体的实现。假如现在有一个需求,你要限制用户在某个时段不能登陆,那么你只需要封装好方法,然后在获取令牌之前插入一段调用方法的代码即可。控制器层不去处理逻辑判断和业务实现有助于后期的扩展和代码简洁性,在某些复杂的业务接口,如果在控制器层涉及过多的逻辑判断和业务处理必然导致后期的维护和扩展上的问题,而且也不便于理解执行流程。

# 数据库模型规范

ThinkPHP 框架提供了强大的ORM实现,通过对使用ThinkPHP内置的模型功能,可以让你高效、方便的操作数据库的各种操作即便你不懂SQL语句。基于框架的模型功能,可以轻松、灵活的定义模型之前的关联关系,如一对多,多对多,远程一对多等,同时可以实现预查询,解决关联查询的n+1的性能问题。

项目已经为你打包了核心模型,他们分别是LinUser、LinAuth、LinGroup、LinLog,模型中已经包含了常用的功能,当然你可以自己创建一个模型类并继承他们来实现扩展或者重写里面的方法。

更多关于ThinkPhPORM的用法可以参考ThinkPHP TP5.1完全开发手册——模型

# 路由

User类下面,我们定义了一个login方法,也就是一个Api,既然是Api,就必须要有一个与之对应的访问url,前端在访问这个url之后,就会路由到我们这个login方法。那么路由如何定义呢?TP5框架提供了很多种的路由定义方式,但总的来说分为注解路由配置路由两种,从字面意思来理解也很好理解,前者就是在方法上面,像写注释一样给方法写入一段注释,框架就会解析然后定义一条路由。而后者,就是通过一个配置文件来定义路由,配置文件的路径在项目根目录下的/route/route.php,打开后会看到如下内容:

image

打开后你会发现里面已经定义了很多路由,看不懂没关系,这里你只需要知道方法需要有一个对应的路由这种概念即可,这里我们为login方法定义了一条请求类型GET,urlcms/user/login的路由规则,他会转发到api/cms.User/login。当前端发起了一个http请求,请求的地址是cms/user/login,就会走到我们User控制器类下面的login方法并执行方法内的逻辑。

当你在编写其他接口时候,也是按照这种套路,创建控制器类——创建控制器类下面的方法——为方法定义路由。

目前项目源码中路由功能采用的是配置路由,在下一个版本中将会全部替换成注解路由

# 验证器

ThinkPHP框架提供了强大且灵活的验证器,除了内置的一系列验证规则之外还可以自定义验证规则,具体支持的内置验证规则可查看官方开发手册验证器

我们提供了一个已经封装好的验证器基类BaseValidate,位于vendor\lin-cms-tp5\base-core\src\validate

我们推荐开发者在application\api\validate这个目录下去书写你的自定义验证器,且继承于BaseValidate基类

image

定义完验证器后,如下方式调用,如果通过则会继续执行控制器中的逻辑,否则会抛出一个异常并中断执行

    public function login(Request $request)
    {
       (new LoginForm())->goCheck();//调用自定义验证器
        // 省略了一堆逻辑
        return $result;
    }
1
2
3
4
5
6

异常信息

{
    "msg": {
        "password": "密码不能为空"
    },
    "error_code": 66667,
    "request_url": "cms/user/login"
}
1
2
3
4
5
6
7

在某些情况下,(new LoginForm())->goCheck();这种语法显得不够优雅,又或者为了一个并不需要复杂验证的接口实现一个验证器显得有些麻烦,于是LinCMS TP5 在原生框架的基础上实现了注解验证器注解参数验证

注解验证器示例

原本(new LoginForm())->goCheck()的调用方式不需要了,只需要在控制器的注释内容中加入固定格式的注解@validate('自定义验证器类名'),即可实现调用自定义验证器。这里的@validate('LoginForm')相当于调用的\app\api\validate\user\LoginForm去验证

    /**
     * 账户登录
     * @param Request $request
     * @validate('LoginForm') //注解验证器
     * @return array
     * @throws \think\Exception
     */
    public function login(Request $request)
    {
        // (new LoginForm())->goCheck();  # 开启注释验证器以后,本行可以去掉,这里做更替说明
        // 省略了逻辑代码
    }
1
2
3
4
5
6
7
8
9
10
11
12

注解参数验证示例

使用@param('参数名','参数注释','参数规则'),进行单个参数验证
例如:@param('id','ID信息','require|max:1000|min:1')

    /**
     * 查询指定bid的图书
     * @param Request $bid
     * @param('bid','bid的图书','require')
     * @return mixed
     */
    public function getBook($bid)
    {
        $result = BookModel::get($bid);
        return $result;
    }
1
2
3
4
5
6
7
8
9
10
11

这里要求调用时参数bidrequire,就是必须。

更多详细介绍查阅注释验证器文档

# 模型管理和权限管理

阅读本小节前,请确保你一定完成了快速开始的全部内容 本小结使用postman作为 http 测试工具,请确保你有 postman 或类似的 http 测试工具,它是我们后续开发必不可少的工具。

# 架构介绍

Lin 的定位是一整套的 ThinkPHP TP5 CMS 解决方案。对于任何的 CMS 来说,权限这一块都是不可或缺的,因此 Lin 在基础框架中便已经集成了权限模块,它是开箱即用的。

不过 Lin 的权限模块的概念可能与其它的权限框架由些许不同,当然你完全不用担心,因为大部分权限系统的模式都大同小异。

在 Lin 的权限模块中,我们有三个模型类来组成这个这个权限模块。如下:

用户模型(LinUser,数据表名称为 lin_user) 用户是权限系统服务的基本单位,CMS 与一些网站的很大的区别在于,CMS 可能不存在不用登陆便可进入的页面(登陆页除外)。

简而言之,用户是必须的。在源代码model/LinUser.php可以找到LinUser这个类。LinUser类实则是一个数据库模型类,它有一些必要的属性和实用的方法。

权限组模型(LinGroup,数据表名称为 lin_group) 权限组是一个非常重要的概念,权限组是权限分配的基本单位,同时它也是容纳用户的容器,它是用户与权限之间的纽带。

一个用户只能属于一个权限组,超级管理员(super)不属于任何权限组,但超级管理员拥有所有的权限,一个权限组可以拥有多个用户。

权限组也可拥有多个权限,也就是说,在某个权限组的用户拥有该权限组的所有权限。

如果,你还不清楚,请你在源代码model/LinGroup.php中阅读LinGroup这个类的属性。

权限模型(auth_model,数据表名称为 lin-auth) 你可以把一个权限理解成一把钥匙,然你拥有这把钥匙的时候你就可以打开某扇门,而当你没有这把钥匙的时候,你就会被锁在门的外面。

所以对于某个用户,比如说:你,当你拥有某个权限时,你就可以访问某个 API(或多个 API),而当你没有这个权限时,你访问 API 时会得到一个授权失败或禁止的信息。

想了解更详细的细节,请查看源代码model/LinAuth中LinAuth`这个类。

上述中的源代码,请在框架根目录下的vendor\lin-cms-tp5中寻找。

# 基本使用

要实现权限控制,只需要在控制器方法上添加一个固定格式的注解即可实现,下面演示如何使用

  • 控制器方法
    /**
     * @auth('创建用户','管理员','hidden')
     * @param Request $request
     */
    public function register(Request $request)
    {
        (new RegisterForm())->goCheck();

        $params = $request->post();
        LinUser::createUser($params);

        logger('创建了一个用户');

        return writeJson(201, '', '用户创建成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里我们创建了一个控制器方法,用来创建一个后台账户,我们需要给这个方法添加一个访问权限,权限名称叫创建用户,属于管理员权限模块,于是我们在方法的注释中加入@auth('创建用户','管理员','hidden')这一段内容,这样我就为这个方法标识好权限了,这里hidden的作用就是让这个权限信息不会出现在前端查询所有可分配权限时出现,示例代码中我们将所有管理员模块的权限都添加了hidden

  • 权限校验中间件

中间件文件所在路径/application/http/middleware/Auth.php,可阅读中间件源码查看实现逻辑和流程;该中间件在项目根目录/route/route.php中进行了挂载。


Route::group('', function () {
    Route::group('cms', function () {
        // 省略代码
    })

    Route::group('v1', function () {
        // 省略代码
    })

})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();#挂载权限校验中间件auth到路由分组的根节点,根节点下的嵌套分组都会应用。

1
2
3
4
5
6
7
8
9
10
11
12

权限的名称和所属模块是可以自由定义的,但要注意因为lin-cms是前后端分离的项目,你在后端为控制器方法定义的权限内容必须是和你前端定义的权限名称相匹配这样菜单才会正常渲染。另外,管理员模块即便不添加hidden并分配给了分组,前端也不会显示对应菜单。

# 行为日志

LinCMS TP5 通过原生框架的Hook实现了行为日志的记录,在需要记录日志的地方监听logger钩子,即可实现日志记录

public function register(Request $request)
    {
        $params = $request->post();
        LinUser::createUser($params);

        Hook::listen('logger', '创建了一个用户');

        return writeJson(201, '', '用户创建成功');
    }
1
2
3
4
5
6
7
8
9

Hook::listen()接收两个参数,第一个是钩子名称,第二个参数内容。这里我们调用了logger这个钩子,钩子的具体行为我们定义在application\api\behavior目录下的Logger.php文件中,文件内实现了一个run方法

    public function run($params)
    {

        // 行为逻辑
        if (empty($params)) {
            throw new LoggerException([
                'msg' => '日志信息不能为空'
            ]);
        }

        if (is_array($params)) {
            list('uid' => $uid, 'nickname' => $nickname, 'msg' => $message) = $params;
        } else {
            $uid = Token::getCurrentUID();
            $nickname = Token::getCurrentName();
            $message = $params;
        }

        $data = [
            'message' => $nickname . $message,
            'user_id' => $uid,
            'user_name' => $nickname,
            'status_code' => Response::getCode(),
            'method' => Request::method(),
            'path' => Request::path(),
            'authority' => ''
        ];
        LinLog::create($data);
    }
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

run方法主要是实现了字段信息的赋值和拼接,可根据实际情况调整。

# 配置

本项目遵循ThinkPHP的框架原则,即惯例重于配置的原则,系统会按照下面的顺序来加载配置文件(配置的优先顺序从右到左)。

惯例配置->应用配置->模块配置->动态配置

  • 惯例配置:核心框架内置的配置文件,无需更改。
  • 应用配置:每个应用的全局配置文件(框架安装后会生成初始的应用配置文件),有部分配置参数仅能在应用配置文件中设置。
  • 模块配置:每个模块的配置文件(相同的配置参数会覆盖应用配置),有部分配置参数模块配置是无效的,因为已经使用过。
  • 动态配置:主要是指在控制器或者行为中进行(动态)更改配置,该配置方式只在当次请求有效,因为不会保存到配置文件中

# 配置文件和目录

为更好的应对模块化的开发要求,ThinkPHP5.1的应用配置主要包括应用配置目录和模块配置目录,结构如下:

├─config(应用配置目录)
│  ├─app.php            应用配置
│  ├─cache.php          缓存配置
│  ├─cookie.php         Cookie配置
│  ├─database.php       数据库配置
│  ├─log.php            日志配置
│  ├─session.php        Session配置
│  ├─template.php       模板引擎配置
│  ├─trace.php          Trace配置
│  └─ ...               更多配置文件
│  
├─route(路由目录)
│  ├─route.php          路由定义文件
│  └─ ...               更多路由定义文件
│  
├─application (应用目录)
│  └─module (模块目录)
│     └─config(模块配置目录)
│        ├─app.php            应用配置
│        ├─cache.php          缓存配置
│        ├─cookie.php         Cookie配置
│        ├─database.php       数据库配置
│        ├─log.php            日志配置
│        ├─session.php        Session配置
│        ├─template.php       模板引擎配置
│        ├─trace.php          Trace配置
│        └─ ...               更多配置文件
│
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

了解更多关于ThinkPHP5.1 配置

ThinkPHP默认使用的是.php文件的形式来管理配置文件,这种方式存在一定的安全问题和性能问题,这里推荐你在任何PHP项目中都使用Yaconf来管理你的配置文件。

关于Yaconf的安装和配置用法可以参考这里

# 令牌

# 为何选择jwt

最近几年 Restful API 及 SPA(单页面应用) 的盛行,cookie-session 的机制似乎越来越不适合前后端分离的场景。 在分布式和微服务的趋势下,不少人选择在 redis 中存储 session 来达到单点登陆的效果,这无疑增加了成本和开发难度。 于是更多的人转而使用 jwt 来管理用户会话和授权,在 jwt官网介绍其两大使用场景。

Authorization(授权):这是 jwt 应用最为广泛的场景。jwt 将数据加密存储,分发给前端,前端将其放在特定的 header 字段 中(也有放在 params 和 body 中),服务器收到请求后,解析 jwt 判断用户身份,对用户请求进行限权。

Information Exchange(数据交换): jwt 可以通过公钥和私钥对信息进行加密,双方通信后,互得数据。

关于 jwt 的使用及生态请阅读 jwt官网获得更多信息。

# access_token 和 refresh_token

在一般 jwt 应用中,access_token和refresh_token是一对相互帮助的好搭档,前面讲到用户在前端登陆后,服务器会发送 access_token 和 refresh_token 给前端,前端在得到这两个 token 之后必须谨慎存储

access_token :用来用户鉴权,控制用户对接口,资源的访问。access_token 十分重要,它是服务器对前端有力控制的唯一途径,故一般其生存周期十分短,一般在 2 个小时左右,更有甚者,其生命周期只有 15 分钟,在 Lin 中默认 1 个小时。

refresh_token :用户通过登陆后获得 access_token,而 access_token 的生命周期十分短暂,但是用户登陆太频繁会严重影响体验,因此需要一种免登陆便能获取 access_token 的方式。refresh_token 主要用来解决该问题,refresh_token 的生命周期较长,一般为 30 天左右,但 refresh_token 不能被用来用户身份鉴权和获取资源,它只能被用来重新获取 access_token。当前端发现 access_token 过期时,便应通过 refresh_token 重新获取 access_token。

# 项目中的使用

在 Lin 中已经默认集成了 jwt 的机制,你可以通过 HTTP 请求来进行相关的操作。

# 用户获取 access_token 和 refresh_token

path: /cms/user/login

method: post

参数:

name 说明 类型 作用
nickname 用户名 string
password 密码 string

结果:

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzU1MzMyNjMsIm5iZiI6MTUzNTUzMzI2MywianRpIjoiMTlkZWUwNzQtNzUxYi00MjBlLTk3NjAtZDRkMzc3YjdjMjUyIiwiZXhwIjoxNTM1NjE5NjYzLCJpZGVudGl0eSI6InBlZHJvIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.9sNmAV5anxY5N1S1kaXzRRpdjzVX3fX6iI0ZjxGiiVs",
  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzU1MzMyNjMsIm5iZiI6MTUzNTUzMzI2MywianRpIjoiYjU0OWIwZGEtMTE3MS00NzJlLWE0MDMtMDFkMGRkZTRjOTYzIiwiZXhwIjoxNTM4MTI1MjYzLCJpZGVudGl0eSI6InBlZHJvIiwidHlwZSI6InJlZnJlc2gifQ.cBnqEBnome-dMFEueQ8oCJfoXX9_mzQJAGjyeq4bYh8"
}
1
2
3
4

使用:

在请求其他资源或接口时在 header 中,加入Authorization字段,字段值为Bearer加上access_token,注意两个字段中间必须有一个空格,如下:

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzU1MzMyNjMsIm5iZiI6MTUzNTUzMzI2MywianRpIjoiMTlkZWUwNzQtNzUxYi00MjBlLTk3NjAtZDRkMzc3YjdjMjUyIiwiZXhwIjoxNTM1NjE5NjYzLCJpZGVudGl0eSI6InBlZHJvIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.9sNmAV5anxY5N1S1kaXzRRpdjzVX3fX6iI0ZjxGiiVs
1

服务器会解析该字段并得到用户信息,对用户进行鉴权。

# refresh_token 获取 access_token

path: /cms/user/refresh

method: get

参数: 无,注意在 Authorization 中加上 refresh_token。 结果:

{
  "access_token":
    "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzU1MzMyNjMsIm5iZiI6MTUzNTUzMzI2MywianRpIjoiMTlkZWUwNzQtNzUxYi00MjBlLTk3NjAtZDRkMzc3YjdjMjUyIiwiZXhwIjoxNTM1NjE5NjYzLCJpZGVudGl0eSI6InBlZHJvIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.9sNmAV5anxY5N1S1kaXzRRpdjzVX3fX6iI0ZjxGiiVs"
1
2
3

使用

用户获取资源时,Authorization字段的值为Bearer加上access_token,当通过 refresh_token 获取 access_token 时,应将Authorization中的 access_token 替换为 refresh_token,如:

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzU1MzMyNjMsIm5iZiI6MTUzNTUzMzI2MywianRpIjoiYjU0OWIwZGEtMTE3MS00NzJlLWE0MDMtMDFkMGRkZTRjOTYzIiwiZXhwIjoxNTM4MTI1MjYzLCJpZGVudGl0eSI6InBlZHJvIiwidHlwZSI6InJlZnJlc2gifQ.cBnqEBnome-dMFEueQ8oCJfoXX9_mzQJAGjyeq4bYh8
1

服务器会解析该字段,如 refresh_token 字段未过期则会发送新的 access_token。

TIP
当然在 Lin 的前端框架中,已经默认实现了以上机制,你大可不必自己去实现,当然如果你想深入了解,也可在阅读本小节后自行尝试。

最后更新时间: 5/26/2020, 10:51:39 PM