zoukankan      html  css  js  c++  java
  • 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 CoreCommon;
    use CoreRedisService;
    
    /***
     * @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 CoreCommon;
    use CoreRedisService;
    
    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 CoreController;
    use CoreCommon;
    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的使用。

  • 相关阅读:
    关于dependency的scope
    Enum
    vim
    postgres函数
    Static块与线程安全
    查看文件夹及文件大小df du
    mac下安装jdk
    硬盘修复原理
    关于Jquery的Accordion在IE下闪烁,不稳定的问题
    vmare安装linux问题总结4(redhat在启动时卡在sendmail很久)
  • 原文地址:https://www.cnblogs.com/lisqiong/p/11023701.html
Copyright © 2011-2022 走看看