我正在 SegmentFault 上录制一些 视频课程,欢迎购买收看,这是支持我创作更多技术内容的好机会哦。
基于业界最成熟的加密和版本控制工具 —— GPG 和 Git 的密码管理器:Elecpass
标签 #PHP

PHP进阶:1.安全

我决定写一系列教程:PHP进阶——一个论坛系统的实现,这是第一章。

1.0 安全

我们将安全放到了最前边,对于Web应用来说,安全的重要性不言而喻,如果你编写的程序存在漏洞,攻击者将有可能取得服务器的控制权,进行破坏,甚至利用你的站点进一步散播木马。

有些人认为测试性的代码,位于内网的应用,可以暂时不必关心安全,这是非常不负责任的。你很难预见这段代码什么时候会被作为你的应用的一部分,保险的做法是,为你写下的每一段代码负责,通过本章提出的几项原则,这并不麻烦。

  • SQL 注入
  • Shell 注入
  • Eval
  • XSS
  • CSRF
  • 关注官方通告
  • 最小权限原则
  • 中间人攻击
  • Dos 攻击
  • 隐藏信息

SQL 注入

如果你的应用需要将一个包含用户输入的字符串作为代码(如SQL)执行,那么攻击者可以通过构造巧妙的输入,来将额外的代码注入到要执行的字符串中,实现攻击.

下面举一个 SQL 注入的例子, 如果有如下的用户验证的代码:

$result = $db->query("SELECT * FROM `users` WHERE (`name` = '{$_POST['user']}') and (`passwd` = '{$_POST['passwd']}')");
if($result->fetch())
    echo "登录成功";

这段代码从 users 表中查询用户名为 $_POST[‘user’] 且密码为 $_POST[‘passwd’]的记录,如存在这样的记录,就认为登录成功。

但攻击者可以通过将 user 和 passwd 分别构造为如下的字符串进行攻击:

admin
1' OR '1'='1

这样一来,实际执行的SQL变成了这个样子:

SELECT * FROM `users` WHERE (`name` = 'admin') and (`passwd` = '1' OR '1'='1')

就可以绕过验证,实现以管理员登录.

产生SQL注入的原因在于单引号在 SQL 中具有特殊含义,单引号标识着一段字符串的开始和结束,如果用户输入中含有单引号,那么该字符串就会提前结束,剩下的部分就会作为SQL命令来解析.

所以,我们有必要对进入 SQL 语句的所有用户输入进行转义,所谓转义就是让代码中具有特殊含义的字符,失去特殊含义,仅仅表示它本身.

具体来说,我们在SQL中需要转义的字符有:

  • 反斜杠 \
  • NUL \0
  • 换行 \n
  • 回车 \r”
  • 单引号 ‘
  • 双引号 “
  • Ctrl+Z \x1a

还有一些和字符集相关的字符(在特定的字符集中,会有一些同义字).

所幸我们不必自己做这些工作,MySQL提供了一个名为 mysql_real_escape_string() 的函数用于将字符串转义,以安全地嵌入SQL.

而 PDO[1] 里同样有 PDO::quote() 完成同样的工作。

值得注意的是 mysql_real_escape_string() 和 PDO::quote() 的行为有细微的区别,前者只是转义字符串,后者在转义后还会在字符串前后加上单引号。

经过修改的代码会是这样:

$user = $db->quote($_POST['user']);
$passwd = $db->quote($_POST['passwd']);
$result = $db->query("SELECT * FROM `users` WHERE (`name` = {$user}) and (`passwd` = {$passwd})");
if($result->fetch())
    echo "登录成功";

如果攻击者继续构造类似之前的注入字符串,那么数据库会去查询用户名为 admin 且密码为 1' OR '1'='1 的记录,数据库仅仅将这段注入字符串视为它字面的意思,不会作为命令来解析,这样攻击者就不会得逞了.

所以,记住我们的第一个原则:

如果你需要将一段字符串作为代码来执行,那么要对所有进入字符串的用户输入进行转义

用户输入不仅仅在于 $_GET 和 $_POST, 包括HTTP Header, Cookie, 文件都是用户输入。

[1]: PHP Data Object, PHP的新式数据库访问接口, 后文会讲解.

更多SQL注入的例子参见: http://php.net/manual/zh/security.database.sql-injection.php

Shell 注入

如有如下代码:

shell_exec("rm /data/files/{$_POST['file']}");

这段代码会根据用户输入,删除 /data/file/ 下的指定文件.

既然你已经知道了我们的第一个原则,显然我们要对输入进行转义。
于是我们找到了 escapeshellarg(), 它会对进入Shell的参数进行转义,添加空格。

经过修改的代码:

$arg = escapeshellarg("/data/files/{$_POST['file']}");
shell_exec("rm {$arg}");

如果攻击者输入 xxoo;rm -rf /, 那么实际执行的命令是:

rm '/data/files/xxoo;rm -rf /'

可以看到攻击者没有成功。

但是攻击者可以继续尝试输入 ../../etc/passwd, 这下你傻眼了, 因为执行了:

rm '/data/files/../../etc/passwd'

相当于:

rm '/etc/passwd'

删掉了系统的用户数据库.

所以,我们除了要对用户输入进行转义之外,还需要 从逻辑上对输入进行检验

在前面SQL注入的例子中,这种检验是不必要的,因为我们知道数据库里面用户的密码不可能恰巧等于攻击者构造的恶意字符串。

但在这个Shell注入的例子中,我们有这个必要:

if(strstr($_POST['file'], "/") !== false)
{
    echo "文件名非法";
}
else
{
    $arg = escapeshellarg("/data/files/{$_POST['file']}");
    shell_exec("rm {$arg}");
}

这样我们便安全了,如果文件名包含了斜杠,程序会拒绝执行命令.

在Linux下,文件名可以使用除斜杠外的所有字符,如果需要迁移到Windows下,我们还需要做更多工作。

所以请记住我们的第二个原则:

不要相信用户的输入,考虑到每一种可能性

你最好把编写代码的过程,看作“证明这段代码不会出错”的过程。

Eval

包括 PHP 在内的大多数脚本语言都提供了类似 eval 的函数,可以将字符串作为代码来执行。同样,在使用 eval() 时我们也需要考虑注入攻击。

但不同于 SQL 和 Shell, 使用 eval() 的大多数场合都是可以避免的,你需要的可能是匿名函数等机制(后文会介绍), 所以在这里不推荐使用 eval() 函数。

XSS

Cross-site scripting, 跨站脚本攻击,或者你可以认为是 HTML 注入,这种攻击并非针对服务器,而是针对访客。

大多数网页的输出都是 HTML, 这意味着你的 PHP 程序的输出会作为 HTML 代码来显示在用户的浏览器中,而 HTML 中嵌入的 JS 脚本则会被用户的浏览器执行。

例如一个论坛系统显示帖子的代码:

<?php
$post = getPostData($_POST['id']);
?>
<html>
    <head>
        <title><?= $post['title'];?></title>
    </head>
    <body>
        <?= $post['content'];?>
    </body>
</html>

如果有一个帖子的内容是下面这样呢:

<span style="color: red;">帖子内容</span>

显然文字会被显示为红色,这意味着如果帖子中含有 HTML 代码,将会被直接执行,在这个例子中这段 HTML 代码可以认为是无害的,但如果是嵌入一段脚本呢:

<script>alert("帖子内容");</script>

浏览器会执行一段 JS 代码!如果你认为这除了开个玩笑外也是无害的,那么请看下面的输入:

<script>
    $.post("/topic/create/", {"title": "点进来看一看啊~", "content": "<script>" + $("body script:first").html() + "</script>"});
</script>

这段代码会自动发布一个新帖子,其内容就是这段代码本身!这样以来,这个“无害的玩笑”会在整个论坛传播开。

XSS 有持久性和非持久性(反射型)之分,前面介绍的例子属于持久性 XSS, 即已经将攻击代码保存在数据库中,其他人访问这些信息时,HTML代码就会被执行。

而非持久性的意思就是注入代码并未被持久性的保存,而是存在于 URL 中,如:

http://xxoo.xo/show.php?id=<script>alert();</script>

对应的 PHP 代码:

if(!is_numeric($_GET['id']))
    echo "{$_GET['id']} 不是一个数字";

如果你轻易点击了这个连接,那么你将会被执行一段脚本,危险性不再复述。

你想说不会有人点这么显然的链接?其实它可以很隐蔽:

http://xxoo.xo/show.php?id=%3Cscript%3Ealert()%3B%3C%2Fscript%3E

甚至:

http://xxoo.xo/show.php?id=%3C%73%63%72%69%70%74%3E%61%6c%65%72%74%28%29%3B%3C%2F%73%63%72%69%70%74%3E

既然是注入 HTML,那么对策并不复杂:对所有输出到 HTML 的数据进行转义,使用 htmlspecialchars(), 如:

if(!is_numeric($_GET['id']))
{
    $id = htmlspecialchars($_GET['id']);
    echo "{$id} 不是一个数字";
}

好吧,其实前面那么多铺垫这是为了让你了解 XSS 的重要性,当下它们甚至比 SQL 注入更加流行。

CSRF

Cross-site request forgery, 跨域请求伪造。

浏览器本身为 JS 制定的一个“跨域保护”规则,保证了 JS 仅能访问当前站点下的资源。

规则即一个域名下的 JS 无法使用 XHR[4] 访问另一个域名下的资源。

但这个限制并不包括 <img>, <script> 等标记的 src 属性,而且这也不现实,因为一个站点难免引用其他站点的图片等资源。

现在很多站点都使用类似于 /user/logout/ 的链接来实现注销登录的功能。如果有这样一段代码:

<img src="//xxoo.xo/user/logout/" />

那么如果用户访问了这个页面,它在 xxoo.xo 网站就会被注销登录,无论当前站点是不是 xxoo.xo !

这样攻击是非常隐蔽的,因为如果图片加载失败了也只不过是一个小叉而已,还可以通过 CSS 隐藏起来。

上面的例子也许可以认为是无害,因为被注销登录也算不得什么大不了的事情,但如果你继续使用 GET 方式响应非修改性的请求的话,事情就没那么简单了。

记住: 所有修改性的操作都使用POST方式

[2]: XMLHttpRequest, JS 无刷新请求技术的 API 接口。

关注官方通告

有时候,漏洞并不在于我们编写的代码,而在于我们所使用的软件本身,如PHP, 或者Linux, MySQL等等。

虽然这些软件的代码审核和发布过程是非常严谨的,但也难免有漏网之鱼。一些严重的漏洞会在一天之间被传播,利用和修复。

对此我们没有什么对策,毕竟这些软件不是我们自己写的,我们能做的但只有:

  • 使用官方的稳定发行版
  • 及时关注官方通告,安装补丁

在PHP领域,有很多问题是官方在反复提醒,但仍广泛存在的,我们在此指出几点:

关闭 Register Globals

这是 php.ini 中的一个选项(register_globals), 开启后会将所有表单变量($_GET和$_POST)注册为全局变量.

看下面的例子:

if(isAuth())
    $authorized = true;
if($authorized)
    include("page.php");

这段代码在通过验证时,将 $authorized 设置为 true. 然后根据 $authorized 的值来决定是否显示页面.

但由于并没有事先把 $authorized 初始化为 false, 当 register_globals 打开时,可能访问 /auth.php?authorized=1 来定义该变量值,绕过身份验证。

该特征属于历史遗留问题,在 PHP4.2 中被默认关闭,在 PHP5.4 中被移除。

关闭 Magic Quotes

对应 php.ini 中的选项 magic_quotes_gpc, 这个特征同样属于历史遗留问题,已经在 PHP5.4 中移除。

该特征会将所有用户输入进行转义,这看上去不错,我们之前提到过要对用户输入进行转义。

但是 PHP 并不知道哪些输入会进入 SQL , 哪些输入会进入 Shell, 哪些输入会被显示为 HTML, 所以很多时候这种转义会引起混乱。

最小权限原则

让我们试想如果 PHP 出现了一个验证漏洞,导致可以任意执行Shell命令,这将会多么恐怖。事实上这种漏洞在各种服务器软件上时有发生。

虽然我们无法阅读和修改 PHP 的源代码,但我们可以通过给予 PHP 最小的权限,来减少漏洞被利用后的损失。

很多教程在遇到文件权限问题的时候,会很不负责任地让你将文件夹权限设置为 777, 即最宽松权限。

在Linux下,每个文件,每个进程都对应着一个用户,非 root 用户只能控制自己的文件,自己的进程,自己的进程创建的文件也是属于自己的。

每个文件有 9 个权限位,分别对应 自己, 同组用户, 其他用户读取, 修改, 执行 权限

基于这种机制,我们可以将服务器的不同功能划分为不同的用户,实现隔离,即使一个服务被攻击者控制,也不会影响到其他的服务。

事实上各个发行版也是这么做的,Apache2, PHP-FPM[2] 大多是以 www-data 或类似的用户运行的,即使被拿到权限,也只能控制网站的部分,无法执行需要 root 权限的系统级命令。

我们可以进一步对权限进行细分,如果应用不需要写入文件,那么给 Web 目录设置只读权限就可以了。

储存静态资源(图片, CSS, JS等)的目录,不要允许 PHP 执行。

甚至让每一个站点都以单独的用户来运行[3].

如果服务器仅提供 Web 服务,那么防火墙仅开放 80 端口。

对于数据库以及其他服务,我们也需要给予应用最小的权限,为每个应用建立单独的账户,只赋予每个应用读写自己的数据库的权限。

[3]: PHP 在 Apache2 下可能依附于 Apache2进程,在 Nginx 则大多用 PHP-FPM 方式运行。

[4]: Apache2 可使用 apache2-mpm-itk, PHP-FPM 可使用配置文件中的 user 选项。

中间人攻击

在 Web 领域,中间人攻击即服务器和用户之间,存在着一个可以双向篡改数据包的 中间人,它有可能是网络中的一个路由节点,也可能是一个钓鱼网站。

说实话这已经是网络传输层面的问题了,和 PHP 没多大关系,解决中间人攻击的唯一途径就是使用 https 协议,SSL 除了可以防御中间人攻击外,还可以让你的数据以加密的形式传输(HTTP是明文传输的), 目前网络上已经有了一些免费的证书颁发机构(CA), 如 StarSSL.

DoS 攻击

Denial of Service, 阻断服务攻击,通过大量垃圾请求迫使服务器无法工作。

如果你的网站让攻击者无处下手,那么攻击者可能会使出最野蛮的攻击方式,DoS, 比如使用脚本快速访问你的站点,让你的服务器不堪重负。

首先你应当避免你的站点出现性能瓶颈,尽量通过缓存(后文会讲解)来提供内容,减少请求处理时间。

设置 Web 服务器的最大进程数,避免进程过多导致内存不足。

还可以使用如 DDoS deflate[5] 的脚本,自动对攻击 IP 进行封禁,这对单点 DoS 非常有效。

如果攻击者不计成本地使用流量“堵”你的线路,那么你可以选择一些知名的 CDN 服务来过滤请求,隐藏服务器的真实 IP 地址。

隐藏信息

虽然隐藏信息是一种比较消极的防御手段,但也能为攻击者添加一些障碍。

值得隐藏的信息包括:Web服务器版本,脚本引擎(PHP)版本,错误信息等等。

笔记:PHP和PEAR, PHAR, PECL

这三个东西可以算是PHP中的高新技术了, 既”高科技”, 又是新东西.

有PHP开发经验的可能见过这几个词, 但真心估计没几个详细了解过的.

它们有一个共同点, 都是用来管理PHP的扩展的.

PECL

即PHP Extension Community Library, PHP扩展库.

这里的扩展指的是使用C语言编写的动态链接库扩展, 通过php.ini配置, 并随PHP进程被装入内存.

常见的PECL扩展比如:

  • apc – 字节码缓存器
  • xdebug – 调试工具
  • PDO_xxoo – 数据库驱动
  • memcache, mongo – NoSQL数据库驱动
  • markdown – 文本处理器
  • zip – 压缩算法

这种动态链接库又分两种, 一种是用户空间扩展, 如mongo, markdown, 旨在通过C代码提升性能, 为PHP代码提供更好的抽象.

另一种是Zend扩展, 这是对Zend内核(PHP引擎内核)的扩展, 如apc, xdebug.

PECL在Windows上并不好用, 通常的做法是直接下载形式如php_xxoo.dll的已编译好的二进制动态链接库.

下载时还应该选择对应PHP版本号, 处理器指令集, 线程安全性的版本.

据说PECL的基础设施还在建设中, 目前有个简陋的网页(http://downloads.php.net/pierre/)可以下载到一部分编译好的Windows PECL扩展.

而在Linux则方便不少, 比如要安装apc, 只需:

pecl install apc

即可, pecl是PHCL的命令行工具, 可以自动完成扩展的安装(通常是下载源代码后自动编译)工作.

PEAR

即PHP Extension and Application Repository, PHP扩展和应用库.

这里的扩展和应用, 指的是用PHP编写的软件包, 一系列类库性质的PEAR包会被安装到PHP的根目录, 然后你就可以在你的代码中直接包含(require)这些类库.

常见的PEAR包如:

  • phpDocumentor – 文档提取工具
  • PHPUnit – 单元测试框架
  • DB – 数据库封装

PEAR希望创建一个规范化的PHP源代码包仓库, PEAR本身也对源代码的格式提出了一些要求, 以便于让源码包更加通用和规范.

同时PEAR也是PECL的上级项目, PECL是PEAR的一部分, pecl的命令行工具也包含在PEAR中.

#安装标配的源码包:
pear install db
#PEAR也采用了`软件源`的设计, 安装非标配的软件包如phpunit, 需要先添加频道(软件源)
pear channel-discover pear.phpunit.de
pear install phpunit

PHAR

PHAR即PHP Archive, 起初只是PEAR中的一个库而已, 后来在PHP5.3后被重新编写成C扩展并内置到PHP中.

PEAR用来将多个.php脚本打包(也可以打包其他文件)成一个.phar的压缩文件(通常是ZIP格式).

目的在于模仿Java的.jar, 不对, 目的是为了让发布PHP应用程序更加方便. 同时还提供了数字签名验证等功能.

.phar文件可以像.php文件一样, 被PHP引擎解释执行, 同时你还可以写出这样的代码来包含(require) .phar 中的代码.

require("xxoo.phar");
require("phar://xxoo.phar/xo/ox.php");

PEAR中的很多源码包都通过PHAR打包.

bcompiler

提到了PHAR, 我还不得不提一下bcompiler, PHP bytecode Compiler, PHP字节码编译器.

虽然它目前只是个实验性项目. 可以通过pecl安装.

很多PHP编写的商业软件, 有闭源的需要, 通常它们会选择Zend Guard等商业代码加密软件, 它们成熟稳定, 但也价格昂贵.

其实bcompiler就能满足这个需求, 它可以将PHP代码编译成字节码, 虽然保密性不及商业加密软件, 但也很难被轻易修改, 而且因为省去了生成字节码的过程, 会有不少的性能提升, 官方表示编译后体积会减少到原来的20%, 性能会提高30%.

使用bcompiler编译出的PHP字节码文件可以直接使用(就像PEAR那样):

// xxoo.exe 是 xxoo.php 编译后的字节码.
require("xxoo.exe");

题外

有空我写个在线的PHAR和bcompiler编译工具.

Windows下部署 Apache2 + PHP + XDebug + MySQL 开发环境

为什么选择这几个软件?

  • Apache2是老牌的Web服务器, 兼容性和功能都很强大, 因为我们只是开发环境, 不需要考虑性能问题.
  • PHP我们选择了比较新的5.4版本, 丢掉了不少历史包袱, 提供了更多的新特征(如数组简写形式).
  • XDebug是调试利器, 在代码中可以用XDebug提供的函数来追踪调用栈等等. 还可以配合IDE进行断点调试甚至远程调试.
  • MySQL仍是目前与PHP配合最紧密的数据库.

测试环境

Windows 7 SP1 64bit

但为了保证兼容性, 我仍选用了各个软件的32bit版本.

下载软件包

在Windows下可没有那么方便的包管理器, 难道你打算试试360软件管家?

Apache

官网: http://httpd.apache.org/

进入Download页面, 再进入Other files页面, 进入 binaries/win32 文件夹. 选择合适的版本进行下载.

我选择的是Apache2.2 x86 openssl(httpd-2.2.21-win32-x86-openssl-0.9.8r.msi).

PHP

官网: http://www.php.net/

进入Download页面, 点击Windows binaries, 选择合适的版本进行下载.

我选择的是PHP5.4 VC9 x86 Thread Safe(php-5.4.13-Win32-VC9-x86.zip).

Thread Safe是线程安全的意思, 因为Windows版的Apache2是线程模型, 所以我们需要线程安全的版本.

XDebug

官网: http://xdebug.org/

进入Download页面, 选择合适的版本进行下载.

我选择的是XDebug2.2 for PHP5.4 VC9(php_xdebug-2.2.1-5.4-vc9.dll).

MySQL

官网: http://www.mysql.com/

进入Download页面, 点击MySQL Community Server, 选择合适的版本进行下载, 他会邀请你注册个帐号, 但你也可以不注册, 点”No Thanks”就行.

我选择的是MySQL5.6 32bit(mysql-installer-community-5.6.10.1.msi).

这里要数这个MySQL最大了, 200MiB左右…里面自带了一个很炫的GUI管理工具…

配置PHP

我在这里建议把这些软件都单独安装到一个文件夹, 便于维护, 同时不要放在系统分区, 否则配置权限很麻烦.

在这里我选择把他们都安装到了D:\.

然后解压PHP. 把下好的XDebug复制到PHP目录下的ext目录(如D:\PHP\ext\php_xdebug-2.2.1-5.4-vc9.dll).

我们将PHP目录中的php.ini-development重命名为php.ini, 这就是PHP的主配置文件了

XDebug

打开php.ini, 在末尾配置XDebug, 新增:

[XDebug]
zend_extension = D:\PHP\ext\php_xdebug-2.2.1-5.4-vc9.dll
xdebug.remote_enable = 1
xdebug.profiler_enable = 1

这样就打开了远程调试, 只要配置一下IDE就可以进行断点调试了.

其他扩展

然后我们还需要把常用的扩展打开(如MySQL), 在php.ini中查找Dynamic Extensions, 然后取消你需要的扩展前的分号(注释符), 我开启了这些扩展:

extension=php_curl.dll
extension=php_gd2.dll
extension=php_mysql.dll
extension=php_mysqli.dll
extension=php_pdo_mysql.dll
extension=php_pdo_sqlite.dll

分别是: CURL(强大的数据传输工具, 支持HTTP在内的多种协议), GD2(图像处理库), MySQL(经典C风格MySQL接口), MySQLi(面向对象风格MySQL接口), PDO的MySQL和SQLite驱动.

配置Apache2

Apache2安装过程没啥好说的, 途中会让你输入服务器名和管理员邮箱, 随便填就行.

安装好之后会在你的Windows上安装成一个服务, 你可以在服务里面设置它是否开机启动, 同时右下角通知区域会有个Apache2的托盘图标.

PHP支持

然后我们需要配置Apache2使其支持PHP脚本.

打开Apache2的主配置文件(如D:\Apache2\conf\httpd.conf), 末尾追加:

LoadModule php5_module D:/php/php5apache2_2.dll
PHPIniDir "D:/php"

AddType application/x-httpd-php .php

第一行中具体的DLL名称要取决于你的Apache2版本.

URL重写和.htaccess

除此之外还建议开启rewrite模块(URL重写), 以及 .htaccess 支持:

搜索LoadModule rewrite_module, 去掉之前的井号.

搜索<Directory />, 改为:

<Directory />
    Options FollowSymLinks
    AllowOverride All
    Order deny,allow
    Allow from 127.0.0.1
</Directory>

这里设置成了允许本机访问任何目录.

虚拟主机

然后我们可以考虑打开虚拟主机支持, 这样我们可以在本机上依靠域名来建立多个站点.

在httpd.conf中搜索Include conf/extra/httpd-vhosts.conf, 去除该行前的井号.

显而易见, 我们在这里包含了另一个文件, 虚拟主机的配置就保存在这个文件中.

打开该文件(如D:\Apache2\conf\extra\httpd-vhosts.conf), 可以看到默认已经有两个虚拟主机示例了(每个<VirtualHost>就是一个虚拟主机).

我们可以不理会它, 当然删除它们也可以.

然后建立我们自己的虚拟主机, 如添加:

<VirtualHost *:80>
    DocumentRoot "D:/Web/Test"
    ServerName test.2local.tk
</VirtualHost>

可以看到我们建了一个虚拟主机, 根目录是D:/Web/Test, 对应域名是test.2local.tk.

2local.tk这个域名是我申请的一个域名, 它以及它的子域永远指向127.0.0.1, 以方便本地测试, 省着改hosts了.

MySQL

打开MySQL安装程序可以看到一个很炫的安装向导.

这个安装程序中附带了很多插件, 比如for Office的插件, for VS的插件等等. 总之, 依赖条件不满足的插件不装就是了(比如你没装Office, 就不要选择Office插件了).

安装后可以在开始菜单找到名为MySQL Workbench的GUI管理工具, 挺好用的.

精子生于 1995.11.25, 21 岁,英文 ID jysperm.

订阅推送

通过邮件订阅精子的博客日志、产品和项目的最新动态,精子承诺每一封邮件都会认真撰写(历史邮件),有想和精子说的话也可以直接回复邮件。

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-SA 3.0  授权发布。最后生成于 2018-06-13.