1. 程式人生 > >php介面資料安全解決方案(一)

php介面資料安全解決方案(一)

  • 前言
  • 目錄介紹
  • 登入鑑權圖
  • 介面請求安全性校驗整體流程圖
  • 程式碼展示
  • 演示使用者登入
  • 演示獲取使用者資訊
  • 文章完整程式碼地址
  • 後記

前言

目的:

  • 1.實現前後端程式碼分離,分散式部署
  • 2.利用token替代session實現狀態保持,token是有時效性的滿足退出登入,token存入redis可以解決不同伺服器之間session不同步的問題,滿足分散式部署
  • 3.利用sign,前端按照約定的方式組合加密生成字串來校驗使用者傳遞的引數跟後端接收的引數是否一直,保障介面資料傳遞的安全
  • 4.利用nonce,timestamp來保障每次請求的生成sign不一致,並將sign與nonce組合存入redis,來防止api介面重放

目錄介紹



├── Core
│   ├── Common.php(常用的公用方法)
│   ├── Controller.php (控制器基類)
│   └── RedisService.php (redis操作類)
├── config.php (redis以及是否開啟關閉介面校驗的配置項)
├── login.php (登入獲取token入口)
└── user.php(獲取使用者資訊,執行整個介面校驗流程)

登入鑑權圖


介面請求安全性校驗整體流程圖


程式碼展示

common.php

<?php
namespace Core;
/**
 * @desc 公用方法
 * Class Common
 */
class Common{
    /**
     * @desc 輸出json資料
     * @param $data
     */
    public static function outJson($code,$msg,$data=null){
        $outData = [
            'code'=>$code,
            'msg'=>$msg,
        ];
        if(!empty($data)){
            $outData['data'] = $data;
        }
        echo  json_encode($outData);
        die();
    }

    /***
     * @desc 建立token
     * @param $uid
     */
    public static function createToken($uid){
        $time = time();
        $rand = mt_rand(100,999);
        $token = md5($time.$rand.'jwt-token'.$uid);
        return $token;
    }

    /**
     * @desc 獲取配置資訊
     * @param $type 配置資訊的型別,為空獲取所有配置資訊
     */
    public static function getConfig($type=''){
        $config = include "./config.php";
        if(empty($type)){
            return $config;
        }else{
            if(isset($config[$type])){
                return $config[$type];
            }
            return [];
        }
    }

}

RedisService.php

<?php
namespace Core;
/*
 *@desc redis類操作檔案
 **/
class RedisService{
    private $redis;
    protected $host;
    protected $port;
    protected $auth;
    protected $dbId=0;
    static private $_instance;
    public $error;

    /*
     *@desc 私有化建構函式防止直接例項化
     **/
    private function __construct($config){
        $this->redis    =    new \Redis();
        $this->port        =    $config['port'] ? $config['port'] : 6379;
        $this->host        =    $config['host'];
        if(isset($config['db_id'])){
            $this->dbId = $config['db_id'];
            $this->redis->connect($this->host, $this->port);
        }
        if(isset($config['auth']))
        {
            $this->redis->auth($config['auth']);
            $this->auth    =    $config['auth'];
        }
        $this->redis->select($this->dbId);
    }

    /**
     *@desc 得到例項化的物件
     ***/
    public static function getInstance($config){
        if(!self::$_instance instanceof self) {
            self::$_instance = new self($config);
        }
        return self::$_instance;

    }

    /**
     *@desc 防止克隆
     **/
    private function __clone(){}

    /*
     *@desc 設定字串型別的值,以及失效時間
     **/
    public function set($key,$value=0,$timeout=0){
        if(empty($value)){
            $this->error = "設定鍵值不能夠為空哦~";
            return $this->error;
        }
        $res = $this->redis->set($key,$value);
        if($timeout){
            $this->redis->expire($key,$timeout);
        }
        return $res;
    }

    /**
     *@desc 獲取字串型別的值
     **/
    public function get($key){
        return $this->redis->get($key);
    }

}

Controller.php

<?php
namespace Core;
use Core\Common;
use Core\RedisService;

/***
 * @desc 控制器基類
 * Class Controller
 * @package Core
 */
class Controller{
    //介面中的token
    public $token;
    public $mid;
    public $redis;
    public $_config;
    public $sign;
    public $nonce;

    /**
     * @desc 初始化處理
     * 1.獲取配置檔案
     * 2.獲取redis物件
     * 3.token校驗
     * 4.校驗api的合法性check_api為true校驗,為false不用校驗
     * 5.sign簽名驗證
     * 6.校驗nonce,預防介面重放
     */
    public function __construct()
    {
        //1.獲取配置檔案
        $this->_config = Common::getConfig();
        //2.獲取redis物件
        $redisConfig = $this->_config['redis'];
        $this->redis = RedisService::getInstance($redisConfig);

        //3.token校驗
        $this->checkToken();
        //4.校驗api的合法性check_api為true校驗,為false不用校驗
        if($this->_config['checkApi']){
            // 5. sign簽名驗證
            $this->checkSign();

            //6.校驗nonce,預防介面重放
            $this->checkNonce();
        }
    }

    /**
     * @desc 校驗token的有效性
     */
    private  function checkToken(){
        if(!isset($_POST['token'])){
            Common::outJson('10000','token不能夠為空');
        }
        $this->token = $_POST['token'];
        $key = "token:".$this->token;
        $mid = $this->redis->get($key);
        if(!$mid){
            Common::outJson('10001','token已過期或不合法,請先登入系統  ');
        }
        $this->mid = $mid;
    }

    /**
     * @desc 校驗簽名
     */
    private function checkSign(){
        if(!isset($_GET['sign'])){
            Common::outJson('10002','sign校驗碼為空');
        }
        $this->sign = $_GET['sign'];
        $postParams = $_POST;
        $params = [];
        foreach($postParams as $k=>$v) {
            $params[] = sprintf("%s%s", $k,$v);
        }
        sort($params);
        $apiSerect = $this->_config['apiSerect'];
        $str = sprintf("%s%s%s", $apiSerect, implode('', $params), $apiSerect);
        if ( md5($str) != $this->sign ) {
            Common::outJson('10004','傳遞的資料被篡改,請求不合法');
        }
    }

    /**
     * @desc nonce校驗預防介面重放
     */
    private function checkNonce(){
        if(!isset($_POST['nonce'])){
            Common::outJson('10003','nonce為空');
        }
        $this->nonce = $_POST['nonce'];
        $nonceKey = sprintf("sign:%s:nonce:%s", $this->sign, $this->nonce);
        $nonV = $this->redis->get($nonceKey);
        if ( !empty($nonV)) {
            Common::outJson('10005','該url已經被呼叫過,不能夠重複使用');
        } else {
            $this->redis->set($nonceKey,$this->nonce,360);
        }
    }

}

config.php

<?php
return [
    //redis的配置
    'redis' => [
        'host' => 'localhost',
        'port' => '6379',
        'auth' => '123456',
        'db_id' => 0,//redis的第幾個資料庫倉庫
    ],
    //是否開啟介面校驗,true開啟,false,關閉
    'checkApi'=>true,
    //加密sign的鹽值
    'apiSerect'=>'test_jwt'
];

login.php

<?php
/**
 * @desc 自動載入類庫
 */
spl_autoload_register(function($className){
    $arr = explode('\\',$className);
    include $arr[0].'/'.$arr[1].'.php';
});

use Core\Common;
use Core\RedisService;

if(!isset($_POST['username']) || !isset($_POST['pwd'])  ){
    Common::outJson(-1,'請輸入使用者名稱和密碼');
}
$username = $_POST['username'];
$pwd = $_POST['pwd'];
if($username!='admin' || $pwd!='123456' ){
    Common::outJson(-1,'使用者名稱或密碼錯誤');
}
//建立token並存入redis,token對應的值為使用者的id
$config = Common::getConfig('redis');
$redis = RedisService::getInstance($config);
//假設使用者id為2
$uid = 2;
$token = Common::createToken($uid);
$key = "token:".$token;
$redis->set($key,$uid,3600);
$data['token'] = $token;
Common::outJson(0,'登入成功',$data);

user.php

<?php
/**
 * @desc 自動載入類庫
 */
spl_autoload_register(function($className){
    $arr = explode('\\',$className);
    include $arr[0].'/'.$arr[1].'.php';
});

use Core\Controller;
use Core\Common;
class UserController extends Controller{

    /***
     * @desc 獲取使用者資訊
     */
    public function getUser(){
        $userInfo = [
            "id"=>2,
            "name"=>'巴八靈',
            "age"=>30,
        ];
        if($this->mid==$_POST['mid']){
            Common::outJson(0,'成功獲取使用者資訊',$userInfo);
        }else{
            Common::outJson(-1,'未找到該使用者資訊');
        }
    }
}
//獲取使用者資訊
$user = new  UserController();
$user->getUser();

演示使用者登入


簡要描述:

  • 使用者登入介面

請求URL:

  • http://localhost/login.php

請求方式:

  • POST

引數:

引數名 必選 型別 說明
username string 使用者名稱
pwd string 密碼

返回示例

{
    "code": 0,
    "msg": "登入成功",
    "data": {
        "token": "86b58ada26a20a323f390dd5a92aec2a"
    }
}

{
    "code": -1,
    "msg": "使用者名稱或密碼錯誤"
}

演示獲取使用者資訊

簡要描述:

  • 獲取使用者資訊,校驗整個介面安全的流程

請求URL:

  • http://localhost/user.php?sign=f39b0f2dea817dd9dbef9e6a2bf478de

請求方式:

  • POST

引數:

引數名 必選 型別 說明
token string token
mid int 使用者id
nonce string 防止使用者重放字串 md5加密串
timestamp int 當前時間戳

返回示例

{
    "code": 0,
    "msg": "成功獲取使用者資訊",
    "data": {
        "id": 2,
        "name": "巴八靈",
        "age": 30
    }
}

{
    "code": "10005",
    "msg": "該url已經被呼叫過,不能夠重複使用"
}

{
    "code": "10004",
    "msg": "傳遞的資料被篡改,請求不合法"
}
{
    "code": -1,
    "msg": "未找到該使用者資訊"
}

文章完整程式碼地址


點選檢視原始碼

後記


上面完整的實現了整個api的安全過程,包括介面token生成時效性合法性驗證,介面資料傳輸防篡改,介面防重放實現。僅僅靠這還不能夠最大限制保證介面的安全。條件滿足的情況下可以使用https協議從資料底層來提高安全性,另外本實現過程token是使用redis儲存,下一篇文章我們將使用第三方開發的庫實現JWT的規範操作,來替代redis的使用。