前言
目的:
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
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
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
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
return [
//redis的配置
'redis' => [
'host' => 'localhost',
'port' => '6379',
'auth' => '123456',
'db_id' => 0,//redis的第幾個數據庫倉庫
],
//是否開啟接口校驗,true開啟,false,關閉
'checkApi'=>true,
//加密sign的鹽值
'apiSerect'=>'test_jwt'
];
login.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
/**
* @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的使用。