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
namespaceCore;
/**
* @desc 公用方法
* Class Common
*/
classCommon{
/**
* @desc 输出 JSON 数据
* @param $data
*/
publicstaticfunctionoutJson($code,$msg,$data=null){
$outData=[
'code'=>$code,
'msg'=>$msg,
];
if(!empty($data)){
$outData['data']=$data;
}
echo json_encode($outData);
die();
}
/***
* @desc 创建 token
* @param $uid
*/
publicstaticfunctioncreateToken($uid){
$time=time();
$rand=mt_rand(100,999);
$token=md5($time.$rand.'jwt-token'.$uid);
return$token;
}
/**
* @desc 获取配置信息
* @param $type 配置信息的类型, 为空获取所有配置信息
*/
publicstaticfunctiongetConfig($type=''){
$config=include"./config.php";
if(empty($type)){
return$config;
}else{
if(isset($config[$type])){
return$config[$type];
}
return[];
}
}
}
RedisService.PHP<?PHP
namespaceCore;
/*
*@desc Redis 类操作文件
**/
classRedisService{
private$Redis;
protected$host;
protected$port;
protected$auth;
protected$dbId=0;
staticprivate$_instance;
public$error;
/*
*@desc 私有化构造函数防止直接实例化
**/
privatefunction__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 得到实例化的对象
***/
publicstaticfunctiongetInstance($config){
if(!self::$_instanceinstanceofself){
self::$_instance=newself($config);
}
returnself::$_instance;
}
/**
*@desc 防止克隆
**/
privatefunction__clone(){}
/*
*@desc 设置字符串类型的值, 以及失效时间
**/
publicfunctionset($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 获取字符串类型的值
**/
publicfunctionget($key){
return$this->Redis->get($key);
}
}
Controller.PHP<?PHP
namespaceCore;
useCore\Common;
useCore\RedisService;
/***
* @desc 控制器基类
* Class Controller
* @package Core
*/
classController{
// 接口中的 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, 预防接口重放
*/
publicfunction__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 的有效性
*/
privatefunctioncheckToken(){
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 校验签名
*/
privatefunctioncheckSign(){
if(!isset($_GET['sign'])){
Common::outJson('10002','sign 校验码为空');
}
$this->sign=$_GET['sign'];
$postParams=$_POST;
$params=[];
foreach($postParamsas$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 校验预防接口重放
*/
privatefunctioncheckNonce(){
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';
});
useCore\Common;
useCore\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';
});
useCore\Controller;
useCore\Common;
classUserControllerextendsController{
/***
* @desc 获取用户信息
*/
publicfunctiongetUser(){
$userInfo=[
"id"=>2,
"name"=>'巴八灵',
"age"=>30,
];
if($this->mid==$_POST['mid']){
Common::outJson(0,'成功获取用户信息',$userInfo);
}else{
Common::outJson(-1,'未找到该用户信息');
}
}
}
// 获取用户信息
$user=newUserController();
$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是stringtoken
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 的使用.
来源: https://www.cnblogs.com/lisqiong/p/11023701.html