枪与玫瑰

PHP安全编码指南

PHP安全编码指南

一、基础安全建议

  • 拒绝信任原则:所有用户输入及第三方数据源(包括$_GET$_POST$_FILES$_COOKIE$_SERVER部分参数)均不可信
  • 统一编码规范:HTML/PHP/MySQL均使用UTF-8编码
  • SQL构造规范:使用单引号包裹参数
  • 参数对比规范:正确使用===(严格相等)和==(松散相等)
  • 过滤策略选择:优先采用白名单过滤而非黑名单

二、SQL注入防护

1. 注入原理示例

原始SQL语义:

$_GET['name'] = 'abc';
$name = $_GET['name'];
SELECT * FROM admin WHERE user_name = '$name';

生成SQL:

SELECT * FROM admin WHERE user_name = 'abc';

注入后语义:

$_GET['name'] = "abc' OR 'a'='a";

生成恶意SQL:

SELECT * FROM admin WHERE user_name = 'abc' OR 'a'='a';

2. 防护方案

(1)预编译处理(推荐)

使用PDO/mysqli预处理语句:

$stmt = $pdo->prepare('SELECT * FROM admin WHERE user_name = ?');
$stmt->execute([$name]);

(2)传统防护方案

  • 字符转义:UTF-8环境下使用addslashes()替代mysql_real_escape_string()
  • 类型过滤:整型参数使用intval()强制转换

⚠️ GKB编码风险:0xbf27addslashes()转义为0xbf5c27,数据库可能解析为有效单引号

三、XSS防护

1. 攻击原理示例

原始HTML:

<a href="http://www.xxx.com/?page=<?= $_GET['page'] ?>">xss</a>

注入示例:

$_GET['page'] = '1" onclick="alert(1)';

生成恶意代码:

<a href="http://www.xxx.com/?page=1" onclick="alert(1)">xss</a>

2. 防护方案

(1)HTML输出过滤

echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');

(2)URL参数验证

if (!filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)) {
    $url = 'https://' . $url; // 自动补全协议
}

(3)富文本处理

采用HTML Purifier等白名单过滤器:

Python学习笔记-Celery

Python邮件发送与Celery异步任务实现

前言

很久没有写技术博客了,最近因为工作需要,我从PHP转投Python阵营。虽然一开始有很多不习惯的地方(比如Python的语法结构、数据类型处理等),但不得不说,Python确实是一门值得学习的语言,正如那句名言所说:“人生苦短,我用Python”。

本文将分享基于SMTP协议的邮件发送实现,以及如何使用Celery实现异步任务处理。

邮件发送逻辑流程

一、格式化邮件数据

  1. 编码格式选择:建议使用UTF-8编码(Unicode的编码方式之一),以确保邮件能被全球用户正确显示,避免使用GBK等区域性编码。

  2. 邮件类型选择

    • 纯文本邮件
    • HTML格式邮件
    • 带附件的邮件
  3. 邮件地址格式化:安全处理收件人和发件人地址,避免特殊字符导致的安全问题。多个收件人时,格式应为"姓名 “。

  4. 附件处理:将附件转换为Base64编码,便于邮件传输。

重要提示:邮件服务提供商通常会屏蔽HTML中直接引用的图片(如<img src="http://www.mudoom.com/xxx.jpg" />)。若需在邮件中显示图片,可将图片作为附件添加,并在HTML中使用<img src="cid:xxx.jpg" />引用,这样图片才能正常显示。

二、建立SMTP连接

  1. 客户端请求连接:建议使用SSL/TLS加密连接(即使某些邮件服务商允许非加密连接,也应使用加密以保障安全)。

  2. 服务器响应:邮件服务器接受连接请求,建立通信通道。

三、登录邮件账户

  1. 发送账号密码:使用邮箱账户和密码进行身份验证。

注意:如果是自建邮件服务器且允许匿名发送,则可跳过此步骤。

四、发送邮件数据

  1. 使用TCP传输:将邮件数据通过TCP协议发送到邮件服务器。

扩展:Google最近推广的QUIC协议(结合了TCP的稳定性和UDP的高速性)可能是未来网络协议的发展方向。

五、关闭SMTP连接

  1. 主动关闭连接:邮件发送完成后,客户端应主动发送关闭请求(而非等待服务器超时关闭),以更合理地利用邮件服务器资源。

Python邮件发送实现

基于smtplib库实现,针对网络异常情况(如socket.gaierror)进行了异常处理,确保程序不会因异常而终止。

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header

class EmailSender:
    def __init__(self, smtp_server, port, username, password):
        self.smtp_server = smtp_server
        self.port = port
        self.username = username
        self.password = password
        
    def send(self, sender, recipients, subject, content, is_html=False, attachments=None):
        try:
            # 创建邮件对象
            msg = MIMEMultipart()
            msg['From'] = Header(sender, 'utf-8')
            msg['To'] = Header(','.join(recipients), 'utf-8')
            msg['Subject'] = Header(subject, 'utf-8')
            
            # 添加邮件内容
            if is_html:
                msg.attach(MIMEText(content, 'html', 'utf-8'))
            else:
                msg.attach(MIMEText(content, 'plain', 'utf-8'))
            
            # 添加附件
            if attachments:
                for attachment in attachments:
                    with open(attachment, 'rb') as f:
                        mime = MIMEBase('application', 'octet-stream')
                        mime.set_payload(f.read())
                        mime.add_header('Content-Disposition', 'attachment', filename=attachment)
                        mime.add_header('Content-ID', f'<{attachment}>')
                        msg.attach(mime)
            
            # 连接SMTP服务器并发送邮件
            with smtplib.SMTP_SSL(self.smtp_server, self.port) as server:
                server.login(self.username, self.password)
                server.sendmail(sender, recipients, msg.as_string())
            return True
        except Exception as e:
            print(f"邮件发送失败: {str(e)}")
            return False

代码说明:GitHub上提供了完整的实现:https://github.com/ZoaChou/Python-learn/blob/master/application/controller/mailer.py

CentOS下的Redis安装+开机自启动

CentOS上安装Redis

很久没有折腾Redis了,最近为了实现在Oauth的服务端使用Redis缓存code和accessToken之类的数据又开始重操旧业,中间遇上一些问题顺便记录下来避免下回再遇上还得慢慢百度。

首先确定下你的系统版本是否为 CentOS release 6.5 (Final) ,如果不是可能中间会有少许出入。(centos版本号查看方法:cat /etc/redhat-release

然后开始动手下载安装吧:

一、安装

  1. 从官网下载最新的稳定版(你看到这篇文章的时候也许redis已经更新了新的稳定版本,建议先查看是否为最新的稳定版 http://www.redis.io/download
wget http://download.redis.io/releases/redis-2.8.17.tar.gz

2. 解压
tar xzf redis-2.8.17.tar.gz

3. 编译(编译的时间可能会有些长,可以去喝杯茶)
cd redis-2.8.17
make
  1. 测试是否安装成功(这个测试会比较花时间,非必要步骤,不赶时间的建议还是测试一下)
make test

有可能你会遇上 You need tcl 8.5 or newer in order to run the Redis test 这个错误提示,好吧,我们再更新一下tcl(这个是已经下好放百度云盘的大伙儿可以下好传服务器上,需要别的可以自行下载)

4.1 下载

tcl8.6.1-src.tar.gz

4.2 解压安装

tar xzf tcl8.6.1-src.tar.gz
cd tcl8.6.1/unix/
./configure
make
sudo make install

4.3 回到redis刚刚的安装目录 make test 试试吧。

  1. 复制/移动需要的文件到你的server目录
sudo mkdir /Your Server/redis
sudo mkdir /Your Server/redis/src
cd redis-2.8.17
sudo cp redis.conf /Your Server/redis
sudo cp src/redis-server /Your Server/redis/src
sudo cp src/redis-benchmark /Your Server/redis/src
sudo cp src/src/redis-cli /Your Server/redis/src
  1. 修改redis配置(我只改了守护进程模式启动)
vim /Your Server/redis/redis.conf
daemonize = yes

二、配置开机自启动

(网上有些教程说需要修改内核文件配置vm.overcommit_memory = 1,看下了这个参数的作用是"强占"内核的做法,感觉似乎不妥而且也没找到原文件有这个配置项,所以这步不做)

Redis常用操作类

Redis封装类

这是一个封装好的redis常用类,function里面并没有把所有的都封装,只是把最近项目用到的都封装了而已,这里有个比较完整的,需要可以查看。

因为最近做的项目是基于ThinkPHP框架,所以有少量方法是用的TP的内置方法,当然这一部分很少而且应该有基础的PHPer都能看懂。当然,正是因为是基于TP写的类,所以这个类也可以直接替代官方的redis类使用(官方的方法很少而且不好使)。

另外如果在使用过程中出现redis连接超时的情况各位可以尝试修改PHP的socket超时时间来解决:

ini_set('default_socket_timeout', -1); //避免redis超时无法连接

差不多就这些,最后,奉上redis常用操作类:

<?php
/**
 * User: 佐柱
 * Date: 14-4-17
 */
namespace Admin\Model;
use Think\Model;
class RedisModel extends Model{
    protected $redis = null;
    protected $redisList = 'searchList';
    /*
     * 单例模式初始化Redis
     */
    public function _initialize(){
        //单例模式
        if($this->redis === null){
            $this->redis =  new \Redis();
            if(C('REDIS_CTYPE') == 1){
                $this->redis->connect(C('REDIS_HOST'), C('REDIS_PORT'), C('REDIS_CACHE_TIMEOUT'));
            }else{
                $this->redis->pconnect(C('REDIS_HOST'), C('REDIS_PORT'), C('REDIS_CACHE_TIMEOUT'));
            }
            $this->redis->setOption('Redis::OPT_PREFIX', C('REDIS_PREFIX'));//设置每一个key的前缀,区别其他APP
        }
    }
    /*
     * 覆盖存入key-value
     * param string $key 键
     * param string/array $value 值
     * param int    $liveTime 有效时间
     * return true成功,false失败
     */
    public function set($key,$value,$liveTime = 0){
        if(empty($key) || empty($value)){
            $this->error = 'key或value不能为空';
            return false;
        }
        $key = $this->securityFilters($key);
        $value = $this->securityFilters($value);
        //redis的value只能是非数组,数组数据转化成json格式存入
        $value = is_array($value) ? json_encode($value) : $value;
        if($liveTime == 0){
            $this->redis->set($key,$value);
        }else{
            $liveTime = $this->securityFilters($liveTime,true);
            $this->redis->setex($key,$liveTime,$value);
        }
        return true;
    }
    /*
     * 批量覆盖存入key-value
     * param array $array_set 需要存入的数据,格式如下
     * array('key'=>'value')
     * array(0=>array(key =>'key',value =>'value',liveTime =>'liveTime'),1=>array('key','value','liveTime'))
     * param boolean $isLiveTime 是否存在过期时间
     * return array('success' => 0,'failure' => 0)成功,false失败
     */
    public function sets($array_set,$isLiveTime = false){
        $success = 0;//成功条数
        $failure = 0;//失败条数
        if(is_array($array_set)){
            if(!$isLiveTime){//不需要存入过期时间的操作
                //原子性操作
                if($this->redis->mset($array_set)){
                    $success = count($array_set);
                }else{
                    $failure = count($array_set);
                }
            }else{//需要存入过期时间的操作
                unset($value);
                foreach($array_set as $value){
                    if($this->set($value['key'],$value['value'],$value['liveTime'])){
                        $success++;
                    }else{
                        $this->error .= '失败key:'.$value['key'].';';
                        $failure++;
                    }
                }
            }
            return array('success' => $success,'failure' => $failure);
        }else{
            $this->error = '数据格式错误';
            return false;
        }
    }
    /*
     * 返回剩余key生存时间
     * param string $key key
     * return int 剩余生存时间
     */
    public function tll($key){
        $liveTime = $this->redis->tll($key);
        return $liveTime;
    }
    /*
     * 不覆盖存入key-value
     * param array $array_set 需要存入的数据,格式如下
     * array('key'=>'value')
     * array(0=>array(key =>'key',value =>'value',liveTime =>'liveTime'),1=>array('key','value','liveTime'))
     * param boolean $isLiveTime 是否存在过期时间
     * return array('success' => 0,'failure' => 0)成功,false失败
     */
    public function setnx($key,$value,$liveTime = 0){
        if(empty($key) || empty($value)){
            return false;
        }
        $key = $this->securityFilters($key);
        $value = $this->securityFilters($value);
        $value = is_array($value) ? json_encode($value) : $value;
        $this->redis->setnx($key,$value);
        if($liveTime != 0){
            $liveTime = $this->securityFilters($liveTime,true);
            $this->redis->expire($key,$liveTime);
        }
        return true;
    }
    /*
     * 将给定key的值设为value,并返回key的旧值
     * param string $key key
     * param string $newValue 新value
     * return string成功,false失败
     */
    public function getset($key,$newValue){
        $oldValue = '';
        if(!empty($key) && !empty($newValue)){
            $newValue = $this->securityFilters($newValue);
            $newValue = is_array($newValue) ? json_encode($newValue) : $newValue;
            $oldValue = $this->redis->getset($key,$newValue);
            return $oldValue;
        }else{
            $this->error = 'key和value不能为空';
            return false;
        }
    }
    /*
     * 取出value
     * param string $key 键
     * return string成功,false失败
     */
    public function get($key){
        $value = $this->redis->get($key);
        //判断是否为json格式的数据,是则转化回数组
        $value = is_null(json_decode($value)) ? $value :json_decode($value,true);
        return $value;
    }
    /*
     * 批量取出value
     * param array $keys 键
     * return array成功,false失败
     */
    public function gets($keys){
        if(is_array($keys)){
            $tmpValues = $this->redis->mget($keys);
            foreach($tmpValues as $param){
                $values[] = is_null(json_decode($param)) ? $param :json_decode($param,true);
            }
            return $values;
        }else{
            $this->error = '数据格式错误';
            return false;
        }
    }
    /*
     * 是否存在指定key的value
     * param string $key key
     * return true存在,false不存在
     */
    public function exists($key){
        if(!empty($key)){
            $res = $this->redis->exists($key);
            return $res ;
        }else{
            $this->error = '查询key不能为空';
            return false;
        }
    }
    /*
     * 查看所有key-value
     * return array
     */
    public function showKey(){
        $this->redis->keys(C('REDIS_PREFIX').'*');
    }
    /*
     * 删除key
     * param string/array $key key
     * return true成功,false失败
     */
    public function rm($key){
        return $this->redis->del($key);
    }
    /**
     * 数据进栈尾
     * @param array list 进栈数据
     * @param bool isLoop 是否开启循环进栈,true为开启,false为关闭,默认关闭
     * @return string,flase 插入的数据ID,操作出错时返回false
     */
    public function push($list,$isLoop = false){
        if(!$isLoop){
            if (!empty($list)){
                if($list){
                    $this->redis->rPush($this->redisList,$list);
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        } else {
            if (!empty($list)) {
                $ids = array();
                if(isset($value))    unset($value);
                foreach ($list as $key=>$value){
                    $res = $this->push($value,false);
                    if ($res) {
                        $ids[$key] = $res;
                    } else {
                        $ids[$key] = false;
                    }
                }
            }else {
                return false;
            }
        }
    }
    /**
     * 数据出栈
     * @return array,false 栈内数据,栈为空时返回false
     */
    public function pop(){
        $list = $this->redis->lPop($this->redisList);
        if($list){
            return $list;
        } else {
            return false;
        }
    }
    /**
     * 获取栈中还有数据数目
     * @return int,false 返回栈内数据数,栈为空时返回false
     */
    public function listNum(){
        $len = 0;
        $len = $this->redis->lSize($this->redisList);
        if($len){
            return $len;
        } else {
            return false;
        }
    }
    /**
     * 循环取出栈中的所有数据
     * @return array,flase 邮件信息,取完则返回false
     */
    public function fetch_row(){
        $list = array();
        $i = 0;
        while ($this->redis->lGet($this->redisList,$i)){
            $list[] = $this->redis->lGet($this->redisList,$i);
            $i++;
        }
        if (!empty($list)) {
            return $list;
        } else {
            return false;
        }
    }
    /*
     * 清空所有key-value
     */
    public function clear(){
        $this->redis->flushAll();
    }
    /*
     * 字符串、数字安全过滤
     * param string $unsafe 需要过滤的字符串或数字
     * return string|int 过滤后的字符或字符串
     */
    protected function securityFilters($unsafe,$toInt = false){
        if(!$toInt && !is_int($unsafe)){
            if(is_string($unsafe)){
                $safe = addslashes(trim($unsafe));
            }elseif(is_array($unsafe)){
                unset($key);
                unset($value);
                foreach($unsafe as $key => $value){
                    $safe[$this->securityFilters($key)] = $this->securityFilters($value);
                }
            }else{
                    $this->error = '非法数据类型!';
                    return null;
            }
        }else{
            $safe = intval($unsafe);
        }
        return $safe;
    }
}

转载请注明文章出处:https://www.mudoom.com 谢谢:)

安装Redis+PHP连接Redis

安装Redis + PHP连接Redis

花了两天的时间,看了无数的English code,终于在自己的笔记本上装上了Redis,下面奉上教程。

首先,不懂Redis干嘛用的请先百度…

下载Redis的windows32位客户端:windows32位Redis客户端

下载后建议解压到web目录之类的地方,譬如:D:\WWW\Redis

为了省去用CMD进入文件夹的麻烦可以直接进入到你的Redis文件夹下shift+鼠标右键(如右图)

http://blog.sky31.com/wp-content/uploads/2014/03/GIGXT_UPZ9PE@A9FX5-300x144.jpg

接着输入以下指令(redis.conf为redis的配置文件,有需要的可以修改过后运行,这个是我从网上copy下来改好能用的):

http://blog.sky31.com/wp-content/uploads/2014/03/QQ%E5%9B%BE%E7%89%8720140304215540-300x92.jpg

如果你能看到CMD显示以下内容,恭喜你,你的Redis服务端已经能用了:

http://blog.sky31.com/wp-content/uploads/2014/03/91@_L9N2YHZHI2PASAN-300x196.jpg

以后要使用都可以用这个指令开启Redis,当然窗口不能关闭,关闭窗口Redis会停止运行。

Then,建立PHP到Redis的连接,使PHP能够直接往Redis里发送数据:

请运行phpinfo()查看以下内容:PHP版本号、TS or NTS、VC版本

http://blog.sky31.com/wp-content/uploads/2014/03/DDKDSKWBJLIKIJCKFS-300x50.jpg

请选择对应的版本下载

PHP5.4连接Redis所需DLL (我自己用的是XAMPP,所以略不同)

php5.3连接Redis所需DLL (WAMP的一般用这个)

下载后请将解压得到的DLL文件copy到php/ext目录下,譬如我的是D:\xampp\php\ext,WAMP的一时想不起具体路径了,请直接搜索文件夹。(PHP5.4下需要两个一起使用,所以我的有两个DLL)

http://blog.sky31.com/wp-content/uploads/2014/03/QQ%E5%9B%BE%E7%89%8720140304221615-300x107.jpg