心灵有约技术实录

PSY 简介

“心灵有约”是一款基于微信企业号的Web应用,用于在校师生进行心理咨询的在线预约和受理,详见:心理有约 - 使用手册 。系统已通过学校网络中心的安全评估和性能测试,于五月份在校企业号上线。截止目前已有 1200+ 师生注册使用,完成预约十余项。

系统已申请一项软件著作权。

2193971889.png

技术架构简述

系统采用前后端分离模式进行开发。

前端使用的是基于Bootstrap的开源 UI 模板 AdminLTEV3版本,配合其它前端工具;

后端框架为开源接口框架 PhalApi,好处是可以快速且规范地写业务并发布接口,主要借助此框架完成业务处理的分层和接口封装。

由于后端框架 API 层只负责接口发布,于是自行结合签名认证PHP-NoCSRF 自行造了一个接口验证的轮子 改良 NoCSRF 实现对 PHP 后端接口的安全验证

身份认证基于企业微信进行开发,可以实现外来身份识别和校内用户自动注册。

运行于CentOS上的LAMP

身份认证

身份认证主要由身份控制脚本和用户登录、注册接口组成。踩过无数坑之后造的轮子。

流程描述为:访问页面 - 获取微信用户身份 - 判断用户身份 - 查询、插入数据库 - 返回用户信息 - 重定向到访问页面

image-20190814162137728.png

用户身份控制脚本封装为了 Header 类,只能由统一入口进行调用,且用户只能通过统一入口访问应用,通过 URL 后缀参数定向到不同页面。这样设置的原因:

  1. 将用户身份验证模块剥离出来,业务界面专门负责内容渲染,而统一入口配合 Header 脚本管控用户身份。
  2. 微信授权中code的获取,需要通过页面访问来实现,而回调的也是一串带有很多参数的 URL,这些参数会带来很多意想不到的 bug,产生的 bug 也是驱动“设置统一入口”的原因。设置统一入口后,身份验证成功后便重定向到业务页面。
<?php
/**
 * 唯一入口
 * Created by PhpStorm.
 * User: Aris
 * Date: 2019-03-03
 * Time: 21:41
 */
require_once"../vendor/autoload.php";
use App\Common\Header;

// http://xxxxx/psy/pages/index.php?href=main
if(isset($_GET['href'])){
    $href = $_GET['href'];
}else{
    $href = 'submit';
}

$t_url='http://'.$_SERVER['HTTP_HOST'].'/psy/pages/index?href='.$href;
$header = new Header();
$header->index($t_url);

header("location:$href");

Header.php

<?php
/**
 * Created by PhpStorm.
 * User: Aris
 * Date: 2018/12/19
 * Time: 上午8:38
 */

namespace App\Common;

use PhalApi\CUrl;

class Header{

    /**
     * 开启session,获取用户信息
     * @param $t_url
     */
    public function index($t_url){
        session_start();

        if(!isset($_SESSION["adminPsy"]) || $_SESSION["adminPsy"] != true){
            $respond = $this->getArray($t_url);
            $_SESSION = $respond;
        }
    }

    /**
     * 获取用户数组
     * @param $t_url
     * @return mixed
     */
    public function getArray($t_url){
        //获取code
        if(!isset($_GET['code'])) {
            $redirect_uri = urlencode($t_url);//页面地址
            echo $t_url;
            //拼接地址
            $url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxxxx&redirect_uri=".$redirect_uri."&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect";
            //重新访问
            header("location:".$url);
            exit;
        }else {
            $code = $_GET['code'];
        }
//        echo "code = ".$code."</br>";
        
        $respond = '';
        //带code请求登陆接口
        $post_url = 'http://'.$_SERVER['HTTP_HOST'].'/psy/public/?s=Mobile_Login/Index';
        try{
            $curl = new CUrl(2);
            $rs = $curl->post($post_url, array( 'code' => $code ), 3000);
            $respond = json_decode($rs, true);
        } catch (\PhalApi_Exception_InternalServerError $ex){}
        return $respond['data'];
    }
}

微信开发

这一部分按照微信官方给出的授权流程进行操作,使用 PhalApi 封装的 CURL 请求类 PhalApiCUrl 进行接口的请求。主要流程如下:

p1.png

流程和普通微信公众号开发一样,只是将一些参数换了名称,比如OpenID在这里叫做UserID。其余的一些配置信息需要从应用管理员控制台获取到。

每个模块流程为:检验参数合法 - 拼接 URL - 调用$curl->get($URL, 3000);

/**
     * 获取用户信息
     * @param $access_token
     * @param $UserID
     * @return array|bool|string
     */
protected function getUserInfo($access_token, $UserID){
        $curl = new CUrl();
        $userInfoURL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token='.$access_token.'&userid='.$UserID;
        if (isset($UserID)) {
            $UserInfo = $curl->get($userInfoURL, 3000);
            $UserInfo= (array)json_decode($UserInfo, true);
            return $UserInfo;
        }
        return false;
    }

接口安全

系统后端所有功能都以接口的形式发布,所有的方法都只能通过调用接口来实现。所以接口的安全性是重中之重。

最开始直接使用的签名验证。后面接触到了防范CSRF攻击,它是借助csrf_token来实现,但是原方法似乎只能对“一个页面访问一个接口”起到保护作用,于是进行改良。再后来就想着能否将二者结合起来,将这个token也用于验签过程中的随机字符串,于是就又开始了造轮子 改良 NoCSRF 实现对 PHP 后端接口的安全验证 。大致流程如下:

p4.png

其它安全

模板布局

这里是参考 ThinkPHP 以及其它带有视图渲染的框架的做法,进行了代码精简和统一性控制。

通过将前端页面的公共部分进行提取(比如手机页面的引用的 css、菜单栏等),封装为header.phpfooter.php等,然后在每个业务页面的头部和尾部直接include,中间部分就直接写content部分了。这样一来直接更改header.php中的菜单栏,即可对所有页面生效。

<?php include 'common/header.php';?>
  <div class="content">
      <!-- ///// -->
  </div>
<?php include 'common/footer.php'?>

业务处理

框架本身分层分得十分好,模型层、业务层、接口层,分别处理份内的事情。数据库由 NotORM 接管,写起来也十分舒服,不过只限于一些简单的操作,复杂的查询或其它业务语句,则要写原生的,不过要按照官方文档的要求写 SQL 避免被注入。

这一篇其实是众多文档之一,还有很多写给用户的、写给后面做运维同学的、测试文档等,给后面的同学交接本系统应该不会很悲剧(注释还是写得比较多的)。。

后期计划

  • 尝试使用 Redis 进行数据缓存(试过用 Redis 存储 session)
  • 尝试使用 Docker 部署
  • Golang 复现

本文链接:https://ariser.cn/index.php/archives/138/
本站文章采用 知识共享署名4.0 国际许可协议进行许可,请在转载时注明出处及本声明!