准备通过写一个留言板来练习PHP,最近打比赛实在是感觉自己啥都不会,还是基础不行;一叶飘零大佬说了,入门web最好的方法还是先从写网站开始,了解运作流程;只顾一味地刷题,物极必反.

实现的功能:

要进行的设计:

环境:


2019.4.25

弄了几乎一天,用最近学的知识做了简单的实现注册登陆功能login_and_register

文件结构:

-login.html 登陆页面,通向register.html和login.php
-login.php 根据接收到的vcode、username与password判断是否登陆成功
-register.html 注册页面,通向login.html和register.php
-register.php 根据接收到的vcode、username与password判断是否注册成功
-vcode.php 生成验证码
-sql.php 封装好了一个用于php操作mysql的类

2019.4.26

了解了session的基本原理,简单实现了用户的登录和登出功能session_login_and_logout

文件结构:

- index.php 根据是否存在$_SESSION['username']来判断用户是否已登陆
- login.php 登陆页面
- logout.php 实现用户登出,并重定向到index.php

2019.4.29

留言板完工MessageBoard

文件结构:

- index.php 留言、显示
- login.php 登录
- logout.php 登出
- register.php 注册
- mysql.php 数据库类
- vcode.php 绘制验证码类
- vcode.txt 验证码存储

总结:这次留言板设计虽然很简陋,几乎只用到了html和php,但至少功能还是实现了;这么简单的一个小应用其实用到的知识点挺多的,比如session机制实现会话控制,控制登录登出;数据库设计时要考虑外键和unique约束用户名;前后端交互显示;还有就是稍不注意就会出现各种bug,比如mysql注入、xss等;以后设计时应考虑的更加周到

logout按钮好有违和感~

php操作数据库

选择数据库api

php连接mysql的三种方式:

这个小项目用pdo来操作数据库,官方说明:

PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。实现 PDO 接口的每个数据库驱动可以公开具体数据库的特性作为标准扩展功能。 注意利用 PDO 扩展自身并不能实现任何数据库功能;必须使用一个 具体数据库的 PDO 驱动 来访问数据库服务。

PDO 提供了一个 数据访问 抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。 PDO 不提供 数据库 抽象层;它不会重写 SQL,也不会模拟缺失的特性。如果需要的话,应该使用一个成熟的抽象层。

从 PHP 5.1 开始附带了 PDO,在 PHP 5.0 中是作为一个 PECL 扩展使用。 PDO 需要PHP 5 核心的新 OO 特性,因此不能在较早版本的 PHP 上运行。

可以在phpinfo()中找到pdo的相关信息:

连接数据库

以前我都是用mysql进行练习数据库的使用,于是把每次进去的用户名和密码给省去了,但在项目中用户名和密码可不能省略

打开phpStudy\PHPTutorial\MySQL\my.ini,去掉skip-grant-tables这个字段(加上后不需要用户名密码即可连接数据库),mysql服务重启后,重置密码为123456,再次进入就要输入用户名密码了:

mysql -uroot -p123456

首先,我需要创建一个数据库:

create database MessageBoard;

之后,创建了一个index.php文件,写入:

<?php
$username = 'root';
$password = '123456';

#数据源名称DSN,包含了请求连接到数据库的信息
$dsn = 'mysql:dbname=MessageBoard;host=127.0.0.1';

#尝试连接,如果连接失败则抛出异常
try{
	#创建一个表示数据库连接的PDO实例
	$pdo = new PDO($dsn, $username, $password);
} catch(PEOException $e){
	echo $e->getMessage();
}

?>

操作数据库

使用创建好的pdo对象来实现对数据表的增、删、改、查;

实现很简单,根据mysql语句,调用pdo对象的方法即可:

<?php
$username = 'root';
$password = '123456';

//数据源名称,包含了请求连接到数据库的信息
$dsn = 'mysql:dbname=MessageBoard;host=127.0.0.1';

try{
	$pdo = new PDO($dsn, $username, $password);
} catch(PEOException $e){
	echo '数据库连接失败!';
}

/*创建表
$sql = 'create table hello(
	    id int unsigned not null auto_increment,
	    email varchar(20) not null,
	    age tinyint unsigned not null,
	    primary key(id)
    	);';
*/

# 查询数据
$sql = 'select * from hello';


/*删除表
$sql = 'drop table hello';
*/

/*添加数据
$sql = "insert into hello(id, email, age) values (1,'123456789@qq.com', 18)";
*/

/*修改数据
$sql = "update hello set email='123@qq.com', age=20 where id=1";
*/

/*删除数据
$sql = "delete from hello where id=1";
*/

$res = $pdo->query($sql);

foreach ($res as $row) {
	echo $row['id']."<br>";
	echo $row['email']."<br>";
	echo $row['age'];
}

/*
$res = $pdo->exec($sql);
var_dump($res);
*/
?>

异常处理

需要设置用户名为不可重复的;在注册的时候,使用的语句为insert into,如果username字段设置了unique,且注册时输入的用户名已存在,默认情况下是不会报错的,此时数据库也没有添加数据;

则可以用pdo对象调用这个方法,根据值是否是'00000'来判断sql语句执行情况(这里的五个0位string类型)

但执行query方法时,并不能用此方法看到是否执行成功,返回的错误码均为'00000'不知道原因是什么…

预处理

验证码设计

GD库的基本使用

php中的GD库可以创建和处理图像

<?php

//创建画布,参数为画布大小
$img = imagecreatetruecolor(400, 400);

//设置颜色,第一个参数为画布,后面三个参数为三原色red、green、blue,用0-255表示深度
$white = imagecolorallocate($img, 255, 255, 255);
$black = imagecolorallocate($img, 0, 0, 0);
$red = imagecolorallocate($img, 255, 0, 0);
$green = imagecolorallocate($img, 0, 255, 0);
$blue = imagecolorallocate($img, 0, 0, 255);


//更改图像背景,图像背景默认为黑色
imagefill($img, 0, 0, $white);

/*画一条线,第二个参数到第四个参数为起点x轴、起点y轴、终点x轴、终点y轴
第一个参数为画布,第四个参数为颜色*/
imageline($img, 0, 0, 400, 400, $red);

//告诉浏览器如何解析图像
header('content-type:image/png');

//输出png图像
imagepng($img);

//释放资源
imagedestroy($img);

运行显示:

验证码的基本实现

还未学到cookie和session的设计,先用文件操作来进行验证码的验证

制作简单的验证码图片就是将随机数绘制到画布上;验证验证码时,可以将随机数写入到文件,之后取出与输入的验证码作对比,一致则验证成功

绘制验证码:

<?php
class Vcode
{
	public function outimage()
	{
		$str = rand(1000, 9999);
		file_put_contents('vcode.txt', $str);
		//创建画布
		$img = imagecreatetruecolor(40, 20);

		//设置颜色
		$white = imagecolorallocate($img, 255, 255, 255);
		$black = imagecolorallocate($img, 0, 0, 0);

		//填充背景为黑色
		imagefill($img, 0, 0, $black);

		//画字符串,第二个参数为字体大小
		imagestring($img, 5, 0, 3, $str, $white);

		//输出图像
		header('content-type:image/png;charset="utf-8"');
		imagepng($img);

		//销毁资源
		imagedestroy($img);
	}
}

$vcode=new Vcode();
$vcode->outimage();

会话管理

无状态HTTP

用户每次发起HTTP请求,服务器都无法识别这个请求是哪个用户发起的;因此,产生了cookie与session,在页面之间传递信息

HTTP请求头:

HTTP响应头:

设置cookie

``` php cookie.php
<?php /第一个参数为cookie名字,第二个参数为cookie的值 time()为当前时间戳,加3600代表cookie有效期为从现在起一小时/

setcookie('username', 'admin', time()+3600);


现在访问这个页面,之后在浏览器设置中可以看到cookie的详细信息:

![](https://i.imgur.com/Sedl8wv.png)

而且可以在network中看到响应头:

    Set-Cookie: username=admin; expires=Mon, 22-Apr-2019 15:50:42 GMT; Max-Age=3600

可以看到返回的cookie的名字和数据,以及失效时间(expires);到了失效时间,浏览器会自动删除cookie;不同浏览器存储cookie位置是不一样的,即不同浏览器不会共享cookie

第一次请求时在请求头中并不会看到cookie这个字段,但刷新页面,会发现请求头中多出来了:

	Cookie: username=admin

## [获取cookie](#获取cookie)

$_COOKIE:超全局数组,获取传递过来的cookie

cookie.php    
``` php     
<?php

var_dump($_COOKIE);

setcookie('username', 'admin', time()+3600);

这样就可以获取到cookie;如果第一次访问是不会显示cookie的,只有服务端发送了cookie给客户端,客户端第二次请求的时候才能显示:

那么,在服务端的哪些位置可以使用$_COOKIE获取cookie呢?

setcookie函数一共有七个参数,除了上面三个常用的参数,第四个参数还可以设置那些路径可以接收cookie;在默认的情况下(不设置第四个参数),同级目录下的文件都可以获取到cookie

比如,现在文件路径为\WWW\MessageBoard\cookie.php,那么在默认情况下,\WWW\MessageBoard\目录下的所有文件都可以通过$_COOKIE获取cookie

现在想让\WWW\下的所有文件获取cookie.php中的cookie,那么只需设置cookie路径为根目录即可:

<?php

setcookie('username', 'admin', time()+3600, '\')

除了用第四个参数设置路径,还可通过设置第五个参数域名设置cookie

setcookie最后两个参数:

删除cookie

可以通过设置cookie过期时间来删除cookie:

<?php
var_dump($_COOKIE);

setcookie('username', '', time()-1);

此时刷新两次页面后,在请求头中便找不到cookie这个字段了;在响应头中发现:

Set-Cookie: username=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

设置session

session.php

<?php

session_start();
/*
首先判断$_COOKIE[session_name()]是否有值,即是否存在session_id;为空则会生成一个session_id,之后将session_id加上前缀`sess_`生成文件,并且通过cookie的方式将session_id传到客户端
如果存在,则会查找这个session_id对应的文件,找到后会反序列化这个文件的内容,将这些内容存到$_SESSION中;如果没找到对应的文件,则会根据这个session_id新建一个session文件
*/

$_SESSION['username'] = 'jack';
/*
给SESSION这个数组里面添加值;脚本结束之后会将名称`username`直接写入session_start()获得的session_id对应的session文件中;数据'jack'根据session.serialize_handler设置的序列化方法存储到session文件中
*/

访问这个页面,会发现响应头有这个字段:

Set-Cookie: PHPSESSID=h0lsdjqmb9juhq1ptp0duucgt6; path=/

此时,在phpStudy\PHPTutorial\tmp\tmp这个目录下会发现一个session文件,文件名为:

sess_h0lsdjqmb9juhq1ptp0duucgt6

sess_开头,里面的内容为:

username|s:4:"jack";

session文件保存的位置或其他有关session的配置可以在php.ini中查看和修改;使用的php版本为5.5.38,则可以在phpStudy\PHPTutorial\php\php-5.5.38中找到对应的php.ini

销毁session

自动销毁

session.php

<?php

session_start();

$_SESSION['name'] = 'jack';

现在,访问这个页面,会在temp目录下找到对应的session文件,查看属性:

创建时间和修改时间是一样的;再次访问这个页面,可以看到修改时间发生了改变:

原因是:再次访问这个页面时,由于通过cookie传过去了session_id,则会反序列化这个session文件的内容,存到$_SESSION中;虽然没有改变键值对数据,但脚本运行完还是会将$_SESSION内容进行序列化存到session文件中,即不管有没有改变,都会覆盖掉以前的数据

php.ini:

即垃圾回收机制触发的条件:经过1440s后文件没有改动,且session_start()执行了1000次,那么就会自动删除符合条件的session文件

主动销毁

在验证时,可以先验证用户的账号密码是否正确,之后将username写入到$_SESSION中,其他页面可以根据$_SESSION是否有这个username来判断是否登陆成功;登出的时候调用session_destroy函数,即可删除后端session文件,即将用户状态设为了登出

session入库

php.ini:

系统默认定义的存储session的方式是file,流程为:

打开
关闭
读取
写入
销毁
回收

若要自定义session存储方式,则需要重写相关函数

cookie与session安全

login.php

<?php

session_start();
if (isset($_SESSION['username'])){
	echo 'login success! welcome ' . $_SESSION['username'] . '<br>';

如果login.php中有这句代码,且temp下生成了对应的session文件sess_jgt7f1d9e7fn6sbsua6cn8l6m6,内容为

username|s:5:"admin";

则可以直接用这个session_id,来实现登陆后的状态

import requests

url = 'http://127.0.0.1/Messageboard/logout/index.php'


headers = {'Cookie':'PHPSESSID=jgt7f1d9e7fn6sbsua6cn8l6m6'}
req = requests.get(url, headers=headers)

print(req.text)

运行结果:

login success! welcome admin<br>

	<form action='./logout.php'>

	<input type='submit' value='logout'></input>

	</form>

那么如何才能防止这种情况呢?

session文件中可以存储许多有关用户的信息,浏览器版本、请求ip等,如果发现这些与用户请求时的不同,则可以创建一个新的会话,从而避免这种危险情况

但如果使用的是cookie机制,而不是session机制:cookie设置的主要信息是一个键值对,且只存在于客户端;如果cookie泄露(XSS),那么攻击者可轻易的模拟用户登录

数据库管理

外键的使用条件:

外键的好处:

表users:

表message:

创建表users语句为:

CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(16) NOT NULL,
  `password` varchar(16) NOT NULL,
  PRIMARY KEY (`id`),
  unique(username)
) ENGINE=InnoDB;

结构:

mysql> desc users;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| username | varchar(16) | NO   | UNI | NULL    |                |
| password | varchar(16) | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+

创建表message语句为:

CREATE TABLE `message` (
  `id` int NOT NULL AUTO_INCREMENT,
  `users_id` int DEFAULT NULL,
  `username` varchar(16) NOT NULL,
  `content` text NOT NULL,
  `time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `users_id` (`users_id`),
  CONSTRAINT `message_table` FOREIGN KEY (`users_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

结构:

mysql> desc message;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| users_id | int(11)     | YES  | MUL | NULL    |                |
| username | varchar(16) | NO   |     | NULL    |                |
| content  | text        | NO   |     | NULL    |                |
| time     | datetime    | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+

留言板页面布局

为了简便,主要用了HTML的< textarea >标签,实现输入与显示

效果:

默认会执行数据库select * from test1操作,将结果输出显示;输入留言点击确定时,会将数据传到php,执行数据库insert into操作;

其他设计

时间:

使用date()函数获取当前时间时,需要设置时区,不知道为什么直接在php.ini中设置失败,在php文件开头加上下面这句话就可以:

ini_set('date.timezone','Asia/Shanghai');

那么将两者拼接即可得到数据库的datetime格式:

date("Y-m-d") . ' ' . date('H:i:s')

想到了一个更好的方法,可以直接在mysql中使用now()函数来实现存储留言时间