前言
目录介绍
登录鉴权图
接口请求安全性校验整体流程图
代码展示
演示用户登录
演示获取用户信息
文章完整代码地址
后记
前言
目的:
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 的使用.
来源: https://www.cnblogs.com/lisqiong/p/11023701.html