Webshell查杀工具

1.百度出品 WEBDIR+ https://scanner.baidu.com/#/pages/intro

web版在线查杀使用方便,上传时推荐使用压缩率比较高的格式,如xz文件

2.WebShellkiller http://edr.sangfor.com.cn/backdoor_detection.html

推荐使用第二种方式检出数量优先,然后人工排除,第一种有方式有很多扫描不出来

3.河马查杀 http://www.shellpub.com/

4 .D盾查杀 http://www.d99net.net

总结

1.开源框架或cms产品及时更新安全补丁,查不出结果时,对比目录结构文件,使用tree命令,然后人工审核,混入一堆乱码(免杀处理)的都是木马.

2.扫描站点的运行木目录,nginx 或apache配置的root目录 .laravel 或tp5框架 运行目录为public ,木马只能放在此目录运行,主要扫描这个目录就可以

3.查找站点下最近修改的php文件.

http://man.linuxde.net/find

  • find ./ -name “*.php” -mtime 0 查找当前目录下24小时内更改的php文件
  • find ./ -name “*.php” -mmin -60 查找当前目录下60分钟内更改的PHP文件

参考

10款常见的Webshell检测工具

网络安全应急响应实战

nginx 让users有权限启动的两种方法

普通用户在restart和reload nginx时,会报错:

nginx: [warn] the “user” directive makes sense only if the master process runs with super-user privileges, ignored in /usr/local/nginx/conf/nginx.conf:2

我又不能给开发人员root权限,没办法,只好这么做。

原因是:默认情况下Linux的1024以下端口是只有root用户才有权限占用

方法一:

所有用户都可以运行(因为是755权限,文件所有者:root,组所有者:root)

chown root.root nginx
chmod 755 nginx
chmod u+s nginx

方法二:

仅 root 用户和 wyq 用户可以运行(因为是750权限,文件所有者:root,组所有者:www)

chown root.www nginx
chmod 750 nginx
chmod u+s nginx

转载原文https://blog.csdn.net/yan7895566/article/details/79876059

Maatwebsite / Laravel-Excel 扩展包导入导出数据

文档 https://laravel-excel.maatwebsite.nl/3.1/getting-started/

github https://github.com/Maatwebsite/Laravel-Excel

环境要求

  • PHP: ^7.0
  • Laravel: ^5.5
  • PhpSpreadsheet: ^1.4
  • PHP扩展已php_zip启用
  • PHP扩展已php_xml启用
  • PHP扩展已php_gd2启用

安装

composer require maatwebsite/excel

laravel5.5已下版本手动添加ServiceProvider config/app.php

'providers' => [
    /*
     * Package Service Providers...
     */
    Maatwebsite\Excel\ExcelServiceProvider::class,
]

添加Facade config/app.php

'aliases' => [
    ...
    'Excel' => Maatwebsite\Excel\Facades\Excel::class,
]

发布配置

php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider" --tag=config
或者
php artisan vendor:publish 然后手动选择

生成config/excel.php配置文件

导出

创建导出类

php artisan make:export UsersExport --model=App/User

导出格式

https://laravel-excel.maatwebsite.nl/3.1/exports/export-formats.html

导出方式

1.直接响应下载文件

use App\Exports\UsersExport;
use Maatwebsite\Excel\Facades\Excel;
use App\Http\Controllers\Controller;

class UsersController extends Controller 
{
    public function export() 
    {
        return Excel::download(new UsersExport, 'users.xlsx');
    }
}

2.存储到磁盘

public function storeExcel() 
{
    // Store on default disk
    Excel::store(new InvoicesExport(2018), 'invoices.xlsx');
    
    // Store on a different disk (e.g. s3)
    Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3');
    
    // Store on a different disk with a defined writer type. 
    Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3', Excel::XLSX);
}
  • 参数1为导出类
  • 参数2为文件名
  • 参数3为laravel存储的磁盘
  • 参数4为导出格式 详情看配置文件

从集合导出

<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;

class UsersExport implements FromCollection
{
    public function collection()
    {
        return User::all();
    }
}
  • 实现 FromCollection 接口 的collection方法
  • collection方法内可以是任何Eloquent ORM的查询 get() first() all() pluck()等方法的返回结果(collection)也可以是通过构造方法传入的collection

从查询导出

namespace App\Exports;

use App\Invoice;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromQuery
{
    use Exportable;

    public function query()
    {
        return Invoice::query();
    }
}
  • 实现Maatwebsite\Excel\Concerns\FromQuery 的 query方法
  • query 方法内return任何Laravel Eloquent ORM的查询构造器的方法,除了获取集合的get() first() all() pluck()等方法

从视图导出

将html表格转换成excel表格,不推荐使用,不灵活,此功能对html结构有要求

队列导出

支持 FromCollection 和 FromQuery

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Illuminate\Contracts\Queue\ShouldQueue;

class UsersExport implements FromCollection, ShouldQueue
{
    /**
     * @return \Illuminate\Support\Collection
     */
    public function collection()
    {
        return User::all();
    }
}

队列直接下载导出 控制器代码

 public function export(Request $request, Excel $excel)
 {
    return $excel->download(new UsersExport(), 'users.xlsx');
 }

队列导出存储到磁盘

public function store(Request $request)
{
   ini_set('memory_limit', '1024M');
   $result = Excel::store(new UsersExport(), 'users_store'.date('YmdHis').'.xlsx', 'public');
}
  • 数据量大时,有时会超出内存限制需要设置内存,后来没复现,没找到原因
  • 从 collection导出的方式,成功后没有发现导出excel文件,后来没复现,没有找到原因

设置表头

class InvoicesExport implements WithHeadings
{   
    public function headings(): array
    {
        return [
            '对应ExcelA列',
            '对应ExcelB列',
            ...
        ];
    }
}

要实现Maatwebsite\Excel\Concerns\WithHeadings的headings方法

每行字段值映射

public function map($code): array
{
    return [
       $A,
       (string) $B,
       0 == $C ? '未使用' : '使用',
    ];
}

要实现 Maatwebsite\Excel\Concerns\WithMapping的map方法

可以对具体值做处理

字段格式化

https://docs.laravel-excel.com/3.1/exports/column-formatting.html

namespace App\Exports;

use PhpOffice\PhpSpreadsheet\Shared\Date;//处理时间格式的类,
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;//处理数字格式
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithMapping;

class InvoicesExport implements WithColumnFormatting, WithMapping
{
    public function map($invoice): array
    {
        return [
            $invoice->invoice_number,
            Date::dateTimeToExcel($invoice->created_at),//时间处理
            $invoice->total
        ];
    }
    
    public function columnFormats(): array
    {
        return [
            'B' => NumberFormat::FORMAT_DATE_DDMMYYYY, 
            'C' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE,
             //I为列头标识格式化成文本常用的比如手机号或订单号会转为科学计数法,这样可以解决
            'I' => NumberFormat::FORMAT_TEXT,
        ];
    }
}

处理日期时,建议在映射中PhpOffice\PhpSpreadsheet\Shared\Date 使用以确保正确解析日期。

设置列宽度

自动设置尺寸

namespace App\Exports;

use Maatwebsite\Excel\Concerns\ShouldAutoSize;

class InvoicesExport implements ShouldAutoSize
{
    ...
}

自定义宽度

namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithColumnWidths;

class InvoicesExport implements WithColumnWidths
{
    public function columnWidths(): array
    {
        //A B 是列的标识头
        return [
            'A' => 55,
            'B' => 45,            
        ];
    }
}

常用配置修改

 /*
  |--------------------------------------------------------------------------
    Heading Row Formatter
  |--------------------------------------------------------------------------
  |
  | Configure the heading row formatter. 
  | Available options: none|slug|custom
  |
  */
  'heading_row' => [
      'formatter' => 'none',//不格式化,这样可以导入中文标题头excel
  ],

   /*
    |--------------------------------------------------------------------------
    | Transaction Handler
    |--------------------------------------------------------------------------
    |
    | By default the import is wrapped in a transaction. This is useful
    | for when an import may fail and you want to retry it. With the
    | transactions, the previous import gets rolled-back.
    |
    | You can disable the transaction handler by setting this to null.
    | Or you can choose a custom made transaction handler here.
    |
    | Supported handlers: null|db
    |
    */
    'transactions' => [
        'handler' => 'null', //取消事物,不回滚,成功导入的数据不会被回滚
    ],

MySql索引的使用总结

所有存储引擎都支持每个表至少 16 个索引,总索引长度至少为 256 字节。

组合索引KEY(col1,col2) 联合主键索引 PARIMARY KEY( col1,col2) 

最左匹配原则

创建了key(k1,k2,k3),相当于创建了(k1)、(k1,k2) 和 (k1,k2,k3) 三个索引

索引全部命中的情况where条件不区分顺序 where k1 = xxx and k2 = xxx and k3 = xxx 等同于 where k3 = xxx and k2 = xxx  and k1 = xxx 查询优化器会处理并使用索引

where k1 = xxx 或 where k1 = xxx and k2 = xxx 使用索引 where k2 = xxx 或 where k3 = xxx 无法使用索引

当查询优化器发现最优索引时,并不会遵循最左匹配原则,而是使用最优查询,如下图k1,k2,k3为连续字段时,不按最左匹配查询还是会命中索引,此时k1,k2,k3被查询优化器当成了一个字段,单次查询k1,k2,k3都会命中

EXPLAIN select * from 3k where k2 = 'sdfdsa'

当k1,k2,k3不在连续中间被其它字段隔开时

相同的查询语句将不会命中索引

联合索引,那么 key 也由多个列组成,同时,索引只能用于查找 key 是否存在(相等),遇到 范围查询(>、<、between、like 左匹配)等就不能进一步匹配了,后续退化为线性查找。因此,列的排列顺序决定了可命中索引的列数。

如有索引 (a, b, c, d),查询条件 a = 1 and b = 2 and c = 3 and d > 4,则会在每个节点依次命中 a、b、c,无法命中 d。也就是最左前缀匹配原则。

不需要考虑 =、in 等的顺序,MySQL 会自动优化这些条件的顺序,以匹配尽可能多的索引列。

如有索引 (a, b, c, d),查询条件 c > 3 and b = 2 and a = 1 and d < 4 与 a = 1 and c > 3 and b = 2 and d < 4 等顺序都是可以的,MySQL 会自动优化为 a = 1 and b = 2 and c > 3 and d < 4,依次命中 a、b。

离散度高原则,选择性(离散性)高的优先,即数据重复率低的列,像性别字段只有男/女两个值,因此选择性很差

离散度的公式是 count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条记录。

索引前缀

字符串字段过长时可以使用,索引前缀,截取字段前一部分的值作为索引

CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));

Range类型 要使用单字段索引

当使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE 或 IN() 运算符中的任何一个将键列与常量进行比较时,可以使用范围

where min <= 1 and max >= 1 语句使用min和max联合索引并不会生效,应该使用min和max的两个单字段索引

索引失效情况总结

查询条件包含 or,会导致索引失效

隐式类型转换,会导致索引失效,where条件左边字段类型为字符串类型时,传入其他类型会触发

like 通配符会导致索引失效,注意:”ABC%” 不会失效,会走 range 索引,”% ABC” 或 “%ABC%” 索引会失效

联合索引,最左匹配原则被中断时

对索引字段进行函数运算,列运算(算术,逻辑,移位等) 注意是对字段做运算,对字段的值做运算时并不影响,如下图

索引字段上使用(!= 或者 < >,not in)时,会导致索引失效

索引字段上使用 is null, is not null,可能导致索引失效

相 join 的两个表的字符编码不同,不能命中索引,会导致笛卡尔积的循环计算

mysql 优化器判断使用全表扫描要比使用索引快,则不使用索引

简单索引和联合索引如何抉择?

能扩展就不要新建索引。如果已有索引 (a),想建立索引 (a, b),尽量选择修改索引 (a) 为索引 (a, b),这样占用空间最小效果一致。

如果已有索引 (a, b),则不需要再建立索引 (a),但是如果有必要,则仍然需考虑建立索引 (b)。

索引尽量少原则

虽说索引可以加速查询,但索引未必是越多越好,因为:

  • 第一点、数据的增删都会涉及到随索引的修改,索引越多维护成本越高,所以频繁进行数据操作的表,不要建立太多的索引;
  • 第二点、索引越多也意味着存储空间需要越大;

因为索引是有代价的,所以用不到的索引,也需要清理掉。

如何定位哪些页面和接口需要加索引?

最简单直接方法是:往数据库里生成 100 万条数据,然后做黑盒测试。

数据有了以后,假装你是一个正常的用户,然后不断地点来点去,做各种操作,点赞、收藏、发文章、评论等,看看哪个动作或者页面加载很慢,就记录下来

如果使用laravel框架可以用telescope或者debug工具查看执行的sql语句,对访问频率高的接口和页面,增加索引,优化查询

参考

盘点那些被问烂了的 Mysql 面试题

MySQL 规约(转自阿里巴巴 Java 开发手册)

官方手册优化与索引

laravel优化教程

composer 加速扩展包—— hirak/prestissimo

在用composer过程中会经常出现安装慢的情况。项目依赖的扩展包过多时,耗时很长,影响部署和开发。

1.是因为网络镜像问题,可以通过设置镜像源解决 https://www.yangliuan.cn/?p=172

2.因为 Composer 是单进程方式下载的, Composer 安装完一个依赖,才回去下载并安装另一个依赖

解决方案使用composer 扩展包 prestissimo

项目地址:https://github.com/hirak/prestissimo

 安装  建议采用全局安装
composer global require hirak/prestissimo

卸载

composer global remove hirak/prestissimo

基准测试 案例

$ composer create-project laravel/laravel laravel1 --no-progress --profile --prefer-dist

288s -> 26s

配置

支持composer 配置项 无需任何特殊配置 composer配置

Composer 配置镜像源

现代php开发都基于composer,由于”墙”的存在,影响了安装或更新扩展包的时候用。因此使用composer时需要配置一下国内镜像

配置镜像

全局方式

composer config -g repo.packagist composer 镜像url

该指令会修改composer 全局配置文件中的镜像url

如图 repositories 配置中 packagist url项

配置当前项目

composer config repo.packagist composer 镜像URL

该指令会在当前项目的composer.json中修改镜像url

国内镜像

镜像名地址赞助商更新频率备注
阿里云 Composer 镜像https://mirrors.aliyun.com/composer/阿里云96 秒推荐
腾讯云 Composer 镜像https://mirrors.cloud.tencent.com/composer/腾讯云24 小时
PHP 国内 Composer 镜像https://packagist.phpcomposer.com仁润股份24 小时不稳定
华为云 Composer 镜像https://repo.huaweicloud.com/repository/php/
https://mirrors.huaweicloud.com/repository/php/
华为云未知未知

如果使用ubuntu和linux开发可以用shell写个切换镜像脚本 composer.sh

Ubuntu 配置环境变量

参考资料 https://www.jianshu.com/p/12fbfa8c7489

以composer 为例

本文修改的是/etc/profile 环境变量,其它环境变量请看参考文章

执行sudo vim /etc/profile

以composer为例,文件在最后一行添加export PATH =”/home/grace/.config/composer/vendor/bin” 如图所示,多个目录用英文冒号分割: .

保存之后重启即可

其它方式生效 source /etc/profile ,会在当前终端生效

后来经过测试,执行source后不需要重启服务器。退出当前会话重新登录即可。如果是图形界面登出桌面,重新登录。如果是终端exit或关闭 ,重新使用ssh登录即可

ubuntu root用户找不到环境变量解决方案

编辑 /root/.bashrc 文件,文件末尾添加 source /etc/profile 保存 执行更新:source /root/.bashrc 或者重启

laravel-cors 扩展包

跨域资源共享 参考 https://www.yangliuan.cn/?p=141

公司采用前后端分类的开发方式,每次部署项目都需要设置nginx配置或添加php header响应非常麻烦,于是在github找了一个用于设置cors跨域的扩展包,很好用。写下来备忘。

github地址https://github.com/barryvdh/laravel-cors

1.composer require barryvdh/laravel-cors

 在laravel项目执行安装,如果使用的是laravel 5.5以下版本 请在config/app.php中的providers 数组中添加 Barryvdh\Cors\ServiceProvider::class,注册该服务提供者

2.php artisan vendor:publish –provider=”Barryvdh\Cors\ServiceProvider”

 执行发布配置文件,该命令会在config目录下生成配置文件

3.根据你的需求讲扩展包中的 \Barryvdh\Cors\HandleCors::class,  中间件添加到 全局中间件 ,中间组,路由中间件中。

如图所示添加到api路由组中间件中

4.根据需求修改配置文件,设置允许的请求来源,请求头 和请求方

 特别注意,该扩展包会在浏览器使用ajax请求时,根据配置自动添加响应头,非ajax请求并不会添加响应头.

  使用不允许的请求源或请求头和方法是会返回403

补充

该扩展包在异常响应时不会响应设置的跨域请求头,尤其是在结合框架自带的表单验证响应422时,会造成跨域

建议使用php原生header写法

laravel 7以后 官方已经集成了该扩展包

Mysql Explain命令

官方手册 :https://dev.mysql.com/doc/refman/8.0/en/using-explain.html

Explain命令 查看语句的执行计划,用于sql语句优化

属性说明:

select_type:select 的语句的查询类型

类型值类型值说明

SIMPLE

简单SELECT(不使用UNION或子查询等)

PRIMARY

最外面的SELECT

UNION

UNION中的第二个或后面的SELECT语句

DEPENDENT UNION

UNION中的第二个或后面的SELECT语句,取决于外面的查询

UNION RESULT

UNION的结果

SUBQUERY

子查询中的第一个SELECT

DEPENDENT SUBQUERY

子查询中的第一个SELECT,取决于外面的查询

DERIVED派生表
DEPENDENT DERIVED派生表依赖于另一个表
MATERIALIZED物化子查询,子查询来自视图
UNCACHEABLE SUBQUERY一个子查询,其结果不能被缓存,必须对外层查询的每一行进行重新评估
UNCACHEABLE UNIONUNION中的第二个或以后的选择,属于不可缓存的子查询(参见不可缓存的子查询)

table:显示这一行的数据是关于哪张表的

partitions: 被查询记录所在的分区,没有返回null,分区功能参考mysql分区功能

type:这列最重要,显示了连接使用了哪种类别,有无使用索引,是使用Explain命令分析性能瓶颈的关键项之一

类型值类型值说明
system该表只有一行(= 系统表)。这是 const 连接类型的特例
const该表最多有一个匹配行,在查询开始时读取。因为只有一行,该行中该列的值可以被优化器的其余部分视为常量。 const 表非常快,因为它们只被读取一次。主键或唯一索引时类型为常量
eq_ref对于前面表中的每个行组合,从该表中读取一行。除了 system 和 const 类型之外,这是最好的连接类型。当连接使用索引的所有部分并且索引是 PRIMARY KEY 或 UNIQUE NOT NULL 索引时使用它
ref所有具有匹配索引值的记录都从这个表中读出,用于前面表中的每一个记录组合。如果连接只使用键的最左边的前缀,或者键不是PRIMARY KEY或UNIQUE索引(换句话说,如果连接不能根据键值选择一条记录),就使用ref。如果使用的键只与几条记录相匹配,这就是一个好的连接类型
fulltext全文索引,类搜索引擎功能
ref_or_null这种连接类型就像ref,但增加了MySQL对包含NULL值的行进行额外的搜索。这种连接类型的优化在解决子查询时最常使用。在下面的例子中,MySQL可以使用一个ref_or_null连接来处理ref_table
index_merge这种连接类型表明使用了索引合并优化。在这种情况下,输出行中的key列包含了所使用的索引的列表,key_len包含了所使用的索引的最长的key部分的列表。更多信息,请参见章节8.2.1.3,”索引合并优化
unique_subquery对于以下形式的某些 IN 子查询,此类型替换 eq_ref:value IN (SELECT primary_key FROM single_table WHERE some_expr)
index_subquery这种连接类型类似于 unique_subquery。它取代了 IN 子查询,但它适用于以下形式的子查询中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr)
range只检索给定范围内的行,使用索引来选择行。输出行中的key列表明使用的是哪个索引。key_len包含了所使用的最长的关键部分。对于这种类型,ref列是NULL
index索引连接类型与ALL相同,只是对索引树进行扫描。这有两种情况。
如果索引是查询的覆盖索引,并且可以用来满足表的所有数据要求,那么只有索引树被扫描。在这种情况下,Extra列显示使用索引。只扫描索引的速度通常比ALL快,因为索引的大小通常比表的数据小。
全表扫描是使用从索引中读出的数据来按索引顺序查找数据行。使用索引不会出现在Extra列中。
当查询只使用属于一个索引的列时,MySQL可以使用这种连接类型。
ALL没有使用任何索引,使用了全表扫描,性能非常差

range示例说明

当使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE 或 IN() 运算符中的任何一个将键列与常量进行比较时,可以使用范围:
SELECT * FROM tbl_name
  WHERE key_column = 10;

SELECT * FROM tbl_name
  WHERE key_column BETWEEN 10 and 20;

SELECT * FROM tbl_name
  WHERE key_column IN (10,20,30);

SELECT * FROM tbl_name
  WHERE key_part1 = 10 AND key_part2 IN (10,20,30);

结果值从好到坏依次是:一般来说,得保证查询至少达到range级别,最好能达到ref,否则就可能会出现性能问题。

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

possible_keys:列指出MySQL能使用哪个索引在该表中找到行

当前查询可用的索引 多个逗号分隔

key:显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL

查询优化器实际选择的索引

key_len:显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好

ref:显示使用哪个列或常数与key一起从表中选择

ref列显示哪些列或常量与列中指定的索引进行比较以 key从表中选择行。

如果值为func,则使用的值是某个函数的结果。要查看哪个功能,请使用 SHOW WARNINGS以下内容 EXPLAIN查看扩展 EXPLAIN输出。该函数实际上可能是一个运算符,例如算术运算符。

rows: rows列表示MySQL认为它必须检查以执行查询的行数,对于InnoDB表,这个数字是一个估计值,不一定准确

filtered: 表按照条件过滤行数的百分比

过滤列表示被表条件过滤的表行的估计百分比。最大值是100,这意味着没有发生过滤的行。从100开始递减的值表示过滤量的增加。rows显示了被检查的行的估计数量,rows × filtered显示了与下面表格连接的行的数量。例如,如果rows是1000,而filtered是50.00(50%),那么与下表中连接的行数是1000×50%=500

Extra:附加信息 包含MySQL解决查询的详细信息,也是关键参考项之一。

这一列包含关于MySQL如何解决查询的额外信息。关于不同值的描述,见EXPLAIN额外信息。

没有与Extra列相对应的单一JSON属性;然而,可能出现在这一列中的值被暴露为JSON属性,或作为消息属性的文本

具体信息看官方文档查询

https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-extra-information

其他一些Tip:

  1. 当type 显示为 “index” 时,并且Extra显示为“Using Index”, 表明使用了覆盖索引。

EXPLAIN ANALYZE

select * 和 select 具体字段差别

跨域资源共享 CORS 详解

转自 阮一峰老师博客 转载只为备忘,侵权请联系yangliuancn@foxmail.com  阅读原文

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

本文详细介绍CORS的内部机制。

一、简介

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

三、简单请求

3.1 基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。


GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。


Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

3.2 withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。


Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。


var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials


xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求

4.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。


var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。


OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

4.2 预检请求的回应

服务器收到”预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。


Access-Control-Allow-Origin: *

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。


XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。


Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

4.3 浏览器的正常请求和回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是”预检”请求之后,浏览器的正常CORS请求。


PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。


Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

补充

最近做一个项目遇到坑点,由于使用的请求头太多,为了省事,跨域请求头直接设置了*

导致了在firefox浏览器和APP的WebView中跨域,Chrome正常

php设置请求头如下

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: *');
header('Access-Control-Allow-Methods: *');

报错误如下,使用options预请求时,来自Access-Control-Allow-Headers 的令牌 ‘authorization’,无效

修正如下,需要指定具体请求头和请求方法

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Authorization, Content-Type, Access-Control-Allow-Headers, X-Requested-With');
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS');