PHP核心之Session
Session(会话)
原理
-
信息传输流程
- 第一次访问,服务器给客户端分配一个空间
- 每个空间都有一个唯一的编号,比如A11
- 将编号A11放到响应头,带回到客户端,保存在cookie中
- 客户端下次访问只能访问带有唯一编号的服务器Session会话空间
-
概念
- Session是服务器端的技术
- Session是基于Cookie技术的
Session操作
-
概述
- 默认情况下,会话不会自动开启,通过
session_start()
开启会话 - 通过
session_id()
获取会话的编号 - 通过
$_SESSION
操作会话 - 会话可以保存除了资源以外的所有类型
- 重复开启会话会报错,一般出现在包含文件中
- 默认情况下,会话不会自动开启,通过
-
session_start()作用
- 没有会话空间就创建一个空间
- 有会话空间就打开空间
<?php
session_start(); //开启会话
@session_start(); //重复开启会话会报错,可以通过错误抑制符来屏蔽错误
$_SESSION['name']='tom'; //保存会话
$_SESSION['age']=20;
echo $_SESSION['name'],'<br>';
echo $_SESSION['age'],'<br>';
echo '会话编号:'.session_id(); //获取会话编号
?>
与会话有关的Session配置
-
主要配置
session.save_path="F:\wamp\tmp\"
Session保存的地址session.auto_start = 1
session自动开启,默认不自动开启session.save_handler = files
会话以文件的形式保存session.gc_maxlifetime = 1440
会话的生命周期是1440秒
-
次要配置
session.name = PHPSESSID
会话命名session.cookie_lifetime = 0
会话编号的过期时间session.cookie_path = /
会话编号整站有效session.cookie_domain =
会话编号在当前域名下有效
销毁会话
- 概念
- 通过
session_destroy()
销毁会话 - 销毁会话就是删除自己的会话文件
- 通过
<?php
session_start();
session_destroy(); //销毁会话
?>
垃圾回收
- 概念
- 会话文件超过了生命周期是垃圾文件
- PHP自动进行垃圾回收
- 垃圾回收的概率默认是1/1000
session.gc_probability = 1
session.gc_divisor = 1000
Session 和 Cookie 的区别
-
Session
- 保存于 服务器
- 存储数据 量大
- 可存储 除了资源以外的所有类型
- 安全
-
Cookie
- 保存于 客户端
- 存储数据 量小(4K)
- 可存储 字符串
- 不安全
禁用Cookie对Session的影响
-
影响
- session是基于cookie的
- 如果禁用cookie,session无法使用
-
解决方案
- 默认情况下,Session只依赖于Cookie,Session的编号只能通过Cookie传输
- 可以设置为Session不仅仅依赖于Cookie
- 设置后,php自动添加get和post传递session_id
session.use_only_cookies = 0 // session不仅仅依赖于cookie
session.use_trans_sid = 1 //允许通过其他方式传递session_id
Session存储到数据库中
- 概念
- Session默认情况下存储到文件中
- 我们可以将Session存储到数据库中
创建sess表
- memory引擎的注意事项
- memory引擎数据存储在内存中,速度快,但是重启服务后数据清空
- memory引擎中的字段不可以是text类型
-- 如果用了text类型就不能使用memory引擎
drop table if exists sess;
create table sess(
sess_id varchar(50) primary key comment '会话编号',
sess_value text comment '会话值',
sess_time int unsigned not null comment '会话产生时间'
)engine=innodb charset=utf8 comment '会话表'
-- memory引擎数据存储在内存中
drop table if exists sess;
create table sess(
sess_id varchar(50) primary key comment '会话编号',
sess_value varchar(2000) comment '会话值',
sess_time int unsigned not null comment '会话产生时间'
)engine=memory charset=utf8 comment '会话表'
更改会话存储(Session入库)
-
通过
session_set_save_handler()
更改存储session_set_save_handler()
必须在session_start()
之前- 有6个回调函数(open,close,read,write,destroy,gc)
- 回调函数read必须返回字符串,其他函数返回布尔值
-
6个回调函数执行的时间
open()
开启会话执行close()
关闭会话执行read()
打开会话后就执行write()
更改会话会话的值和关闭会话之前执行- 如果调用了
session_destroy()
就不会调用write()
- 如果调用了
destroy()
调用session_destroy()
的时候自动执行gc()
垃圾回收的时候自动执行
<?php
//打开会话
function open() {
global $link;
$link=mysqli_connect('localhost','root','','sel');
mysqli_set_charset($link,'utf8');
return true;
}
//关闭会话
function close() {
return true;
}
//读取会话
function read($sess_id) {
global $link;
$sql="select sess_value from sess where sess_id='$sess_id'";
$rs=mysqli_query($link,$sql);
$rows=mysqli_fetch_row($rs);
return (string)$rows[0];
}
//写入会话
function write($sess_id,$sess_value) {
global $link;
$sql="insert into sess values ('$sess_id','$sess_value',unix_timestamp()) on duplicate key update sess_value='$sess_value',sess_time=unix_timestamp()";
return mysqli_query($link,$sql);
}
//销毁会话
function destroy($sess_id) {
global $link;
$sql="delete from sess where sess_id='$sess_id'";
return mysqli_query($link,$sql);
}
//垃圾回收
function gc($lifetime) {
global $link;
$expires=time()-$lifetime; //过期时间点
$sql="delete from sess where sess_time<$expires";
return mysqli_query($link,$sql);
}
//更改会话存储
session_set_save_handler('open','close','read','write','destroy','gc');
//开启会话
session_start();
//session_destroy();
?>
项目封装
- 概念
- 只要访问控制器就会启动session入库
session封装
# Framework\Lib\Session.class.php
<?php
namespace Lib;
class Session{
private $mypdo;
public function __construct() {
session_set_save_handler(
[$this,'open'],
[$this,'close'],
[$this,'read'],
[$this,'write'],
[$this,'destroy'],
[$this,'gc']
);
session_start();
}
public function open() {
$this->mypdo= \Core\MyPDO::getInstance($GLOBALS['config']['database']);
return true;
}
//关闭会话
public function close() {
return true;
}
//读取会话
public function read($sess_id) {
$sql="select sess_value from sess where sess_id='$sess_id'";
return (string)$this->mypdo->fetchColumn($sql);
}
//写入会话
public function write($sess_id,$sess_value) {
$sql="insert into sess values ('$sess_id','$sess_value',unix_timestamp()) on duplicate key update sess_value='$sess_value',sess_time=unix_timestamp()";
return $this->mypdo->exec($sql)!==false;
}
//销毁会话
public function destroy($sess_id) {
$sql="delete from sess where sess_id='$sess_id'";
return $this->mypdo->exec($sql)!==false;
}
//垃圾回收
public function gc($lifetime) {
$expires=time()-$lifetime; //过期时间点
$sql="delete from sess where sess_time<$expires";
return $this->mypdo->exec($sql)!==false;
}
}
?>
基础控制器
# Framework\Core\Controller.class.php
<?php
//基础控制器
namespace Core;
class Controller{
public function __construct() {
$this->initSession();
}
//初始化session
private function initSession(){
new \Lib\Session();
}
}
?>
所有的控制器都继承基础控制器
<?php
namespace Controller\Admin;
//商品模块
class ProductsController extends \Core\Controller{
...
?>
登录模块
创建用户表
drop table if exists `user`;
create table `user`(
user_id smallint unsigned auto_increment primary key comment '主键',
user_name varchar(20) not null comment '用户名',
user_pwd char(32) not null comment '密码',
user_face varchar(50) comment '头像',
user_login_ip int comment '最后登录的IP',
user_login_time int unsigned comment '最后登录的时间',
user_login_count smallint unsigned default 0 comment '登录次数',
is_admin tinyint default 0 comment '超级管理员'
)engine=innodb charset=utf8 comment '用户表';
显示界面
- 目录
- 将HTML模板页面拷贝到View\Admin目录下
- 将images、css 拷贝到Public\Admin目录下
显示登录 注册界面
- 显示登录 注册界面
- 在Controller\Admin目录下创建LoginController.class.php
- 更改login.html、register.html页面中的静态资源路径
- 在HTML中路径要使用绝对路径,从根目录开始匹配
- 在CSS页面图片的路径要使用相对路径,相对于当前页面本身
- 将login.html、register.html页面联通起来
# LoginController.class.php
namespace Controller\Admin;
use Core\Controller; //引入基础控制器
class LoginController extends Controller{
//登录
public function loginAction(){
require __VIEW__.'login.html';
}
//注册
public function registerAction(){
require __VIEW__.'register.html';
}
}
<link rel="stylesheet" href="/Public/Admin/css/pintuer.css">
<link rel="stylesheet" href="/Public/Admin/css/admin.css">
-- login.html跳转到register.html
<input type="button" value="用户注册" class="button button-block bg-main text-big" onClick="location.href='index.php?p=Admin&c=Login&a=register'" />
-- register.html跳转到login.html
<input type="button" class="button button-block bg-main text-big" value="返回" onClick="location.href='index.php?p=Admin&c=Login&a=login'" />
显示后台管理界面
- 显示后台管理界面
- 在Controller\Admin目录下创建AdminController.class.php
- 在admin.html中,更改框架集中的路径
- 更改top.html、menu.html、main.html的静态资源
# AdminController.class.php
namespace Controller\Admin;
class AdminController extends \Core\Controller{
public function adminAction(){
require __VIEW__.'admin.html';
}
public function topAction(){
require __VIEW__.'top.html';
}
public function menuAction(){
require __VIEW__.'menu.html';
}
public function mainAction(){
require __VIEW__.'main.html';
}
}
<frameset rows="95,*" cols="*" frameborder="no" border="0" framespacing="0">
<frame src="index.php?p=Admin&c=Admin&a=top" name="topFrame" scrolling="no" noresize="noresize" id="topFrame" />
<frameset rows="*" cols="180,*" framespacing="0" frameborder="no" border="0">
<frame src="index.php?p=Admin&c=Admin&a=menu" name="leftFrame" scrolling="no" noresize="noresize" id="leftFrame" />
<frame src="index.php?p=Admin&c=Admin&a=main" name="mainFrame" id="mainFrame" />
</frameset>
</frameset>
用户注册
配置文件
# config.php
'app'=>array(
'key' => 'itcast' // 加密密钥
控制器(LoginController)
# LoginController.class.php
public function registerAction(){
//第二步:执行注册逻辑
if(!empty($_POST)){
$data['user_name']=$_POST['username'];
$data['user_pwd']=md5(md5($_POST['password']).$GLOBALS['config']['app']['key']);
$model=new \Core\Model('user');
if($model->insert($data))
$this->success ('index.php?p=Admin&c=Login&a=login', '注册成功,您可以去登陆了');
else
$this->error ('index.php?p=Admin&c=Login&a=register', '注册失败,请重新注册');
}
//第一步:显示注册界面
require __VIEW__.'register.html';
}
完善注册功能
- 概念
- 用户名是不能重复的,但输入用户名以后,通过异步判断一下此用户名是否存在
AJAX
<script>
window.onload=function(){
var req=new XMLHttpRequest(); //创建ajax对象
document.getElementById('username').onblur=function(){
document.getElementById('msg').innerHTML='';
req.open('get','/index.php?p=admin&c=Login&a=checkUser&username='+this.value);
req.onreadystatechange=function(){
if(req.readyState==4 && req.status==200){
if(req.responseText=='1'){
document.getElementById('msg').innerHTML='用户名已经存在';
}
}
}
req.send();
}
}
</script>
...
<div class="field field-icon-right">
<input type="text" class="input" name="username" placeholder="请输入用户名" id='username' />
<span id='msg'></span>
</div>
控制器(LoginController)
public function checkUserAction(){
$model=new \Model\UserModel();
echo $model->isExists($_GET['username']);
}
模型(UserModel)
namespace Model;
class UserModel extends \Core\Model{
//用户存在返回1,否则返回0
public function isExists($name){
$info=$this->select(['user_name'=>$name]);
return empty($info)?0:1;
}
}
用户登陆
- 原理
- 通过用户名和密码找到对应的用户就是登陆成功
控制器(LoginController)
namespace Controller\Admin;
use Core\Controller; //引入基础控制器
class LoginController extends Controller{
//登录
public function loginAction(){
//第二步:执行登陆逻辑
if(!empty($_POST)){
$model=new \Model\UserModel();
if($info=$model->getUserByNameAndPwd($_POST['username'], $_POST['password'])){
$this->success('index.php?p=Admin&c=Admin&a=admin', '登陆成功');
}else{
$this->error('index.php?p=Admin&c=Login&a=login', '登陆失败,请重新登陆');
}
}
//第一步:显示登陆界面
require __VIEW__.'login.html';
}
}
模型(UserModel)
//通过用户名和密码获取用户的信息
public function getUserByNameAndPwd($name,$pwd){
//条件数组
$cond=array(
'user_name' => $name,
'user_pwd' => md5(md5($pwd).$GLOBALS['config']['app']['key'])
);
//通过条件数组查询用户
$info=$this->select($cond);
if(!empty($info))
return $info[0]; //返回用户信息
return array();
}
防止SQL注入
-
原理
- 通过输入的字符串和SQL语句拼接成具有其他含义的语句,以达到攻击的目的
-
防范措施
- 给特殊字符添加转义
- 将单引号替换为空
- md5加密
- 预处理
- 如果确定传递的参数是整数,就需要进行强制类型转换
//单引号添加转义字符
echo addslashes("aa'bb'"),'<br>'; //aa\'bb\'
//字符串替换
echo str_replace("'",'',"aa'bb'"); //aabb
防止FQ
-
FQ
- 通过直接在地址栏输入URL地址进入模板页面
-
解决
- 用户登录成功以后,给用户一个令牌(session),在整个访问的过程中,令牌不消失
登录成功以后,将用户信息保存到会话中
public function loginAction(){
//第二步:执行登陆逻辑
if(!empty($_POST)){
$model=new \Model\UserModel();
if($info=$model->getUserByNameAndPwd($_POST['username'], $_POST['password'])){
$_SESSION['user']=$info; //将用户信息保存到会话中
$this->success('index.php?p=Admin&c=Admin&a=admin', '登陆成功');
....
}
}
}
后台基础控制器(BaseController)
<?php
//后台基础控制器
namespace Controller\Admin;
class BaseController extends \Core\Controller{
public function __construct() {
parent::__construct();
$this->checkLogin();
}
//验证是否登录
private function checkLogin(){
if(CONTROLLER_NAME=='Login') //登录控制器不需要验证
return;
if(empty($_SESSION['user'])){
$this->error('index.php?p=Admin&c=Login&a=login', '您没有登录');
}
}
}
?>
所有的后台控制器都继承后台基础控制器
namespace Controller\Admin;
class AdminController extends BaseController{
....
}
更新登陆信息
控制器(LoginController.class.php)
public function loginAction(){
//第二步:执行登陆逻辑
if(!empty($_POST)){
$model=new \Model\UserModel();
if($info=$model->getUserByNameAndPwd($_POST['username'], $_POST['password'])){
$_SESSION['user']=$info; //将用户信息保存到会话中
$model->updateLoginInfo(); //更新登陆信息
...
}
}
}
模型(UserModel)
- 概念
ip2long()
IPv4转换成整数long2ip()
整数转换成IPv4addslashes()
添加转义字符$_SERVER['REMOTE_ADDR']
获取客户端地址
//更新登陆信息
public function updateLoginInfo(){
//更新的信息
$_SESSION['user']['user_login_ip']= ip2long($_SERVER['REMOTE_ADDR']);
$_SESSION['user']['user_login_time']=time();
$_SESSION['user']['user_login_count']=++$_SESSION['user']['user_login_count'] ;
//实例化模型
$model=new \Core\Model('user');
//更新
return (bool)$model->update($_SESSION['user']);
}
记住密码
- 概念
- 登录成功后,如果需要记录用户名和密码,则将用户名和密码记录在cookie中
public function loginAction(){
//第二步:执行登陆逻辑
if(!empty($_POST)){
//校验验证码
$captcha=new \Lib\Captcha();
if(!$captcha->check($_POST['passcode']))
$this->error ('index.php?p=Admin&c=Login&a=login', '验证码错误');
$model= new \Model\UserModel();
if($info= $model->getUserByNameAndPwd($_POST['username'], $_POST['password'])){
$_SESSION['user']= $info; //将用户信息保存到会话中
$model->updateLoginInfo(); //更新登陆信息
if(isset($_POST['remember'])){
$time= time()+3600*24*7; //记录7天
setcookie('name',$_POST['username'],$time);
setcookie('pwd',$_POST['password'],$time);
}
$this->success('index.php?p=Admin&c=Admin&a=admin', '登陆成功');
}else{
$this->error('index.php?p=Admin&c=Login&a=login', '登陆失败,请重新登陆');
}
}
//第一步:显示登陆界面
$name=$_COOKIE['name']??'';
$pwd=$_COOKIE['pwd']??'';
require __VIEW__.'login.html';
}
<input type="text" class="input" name="username" placeholder="登录账号" value="<?=$name?>" />
...
<input type="password" class="input" name="password" placeholder="登录密码" value="<?=$pwd?>" />
安全退出
- 概念
- 退出:退出的时候不销毁令牌
- 安全退出:退出的时候销毁了令牌(session)
public function logoutAction(){
session_destroy();
header('location:index.php?p=Admin&c=Login&a=login');
}
<a class="button button-little bg-yellow" href="index.php?p=Admin&c=Login&a=logout" target="_top">安全退出</a>