http协议无状态,浏览器基于http协议与服务端通信,问题是:用户登录后,下次还要再登录,因为无状态,每次请求都是从头开始
http协议为啥就不能设计成有状态呢?效率低
2、为了解决http协议状态的问题?浏览器诞生了cookie机制
允许服务端发送响应头,在响应头里设置存放到浏览器里的一些数据,这个机制称之为cookie,服务端生成的、通过响应头里Set-Cookie参数设置/存储到浏览器里的数据也称之为cookie数据
查看cookie数据
下次请求某个域名时,浏览器会找到该域名,然后携带上所有的cookie信息,这便你补了http协议无状态的缺陷
=======cookie机制本身并没有问题,但只用cookie会有安全问题!
在cookie机制诞生之初,最开始的网站,在服务端产生的一些数据,例如用户信息、权限相关等等,都存到了浏览器cookie里
后来发现不安全
1、cookie信息里存放着明文,容易泄露
2、cookie信息存放在客户端,容易被篡改,比如一个普通用户把自己的权限改成了root
1、为了弥补单方面使用cookie而造成的安全问题,大家选择把一个用户会话中需要保存的状态信息例如账号、权限、登录状态等等,都存放到了服务端,这些信息称之为会话数据,也通常被简称为session数据
2、然后服务端会对session数据进行加密,得到一个session的id号,然后只把这个id号设置到客户的cookie里,这样便解决了泄露与篡改的问题
=====session的问题是,限制了集群的规划,服务端集群需要做会话共享
session信息保存于服务端,而负载均衡代理的多个服务端,第一次会话可能打到了web01,然后会话信息存放到了web01机器上
第二次负载均衡将请求转发到了web02,便无法读取该session信息了
解决方法就是将会话信息存放到一个共享的地方,这个共享的地方当然可以是mysql但效率低,用redis会快一些
于是有相当长 一段时间内大家都会有redis来存放共享的session信息,为了防止单点,还要为redis做集群,整个网站硬性依赖redis,是硬性依赖,于是这个点便极大地限制了集群的规模无法分布式扩展
jwt:json web token,一个jwt如下图
jwt分成三部分,三部分由.拼接在一起
1、header: 指定hasah算法与类型
| { |
| "alg": "HS256", |
| "typ": "JWT" |
| } |
| 将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。 |
2、payload:状态信息
| { |
| "sub": "1234567890", |
| "name": "John Doe", |
| "admin": true |
| } |
| 将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。 |
3、signatrue:存放得是“header+payload+盐/密钥”三者通过头里存放得hash算法计算出来得hash值
| |
| HMACSHA256( |
| base64UrlEncode(header) + "." + |
| base64UrlEncode(payload), |
| secret) |
最后把1、2、3得到的值用.拼在一起就行了。
注意:
1、jwt本身是不加密的,存放在header、payload里得内容虽然都经过base64转换过,但base64并不是一种加密算法,可以非常简单的解开,但是日常开发中,你也可以在产生jwt后,再自己加密一次。
2、jwt得机制可以防止篡改,因为只有服务端才知道secret是什么,服务端收到jwt后会对header+payload+secret再做一次hash校验,与seginature对应上了才证明没有被篡改过
客户端收到服务器返回的 JWT后,
1、可以储存在 Cookie 里面,下次与服务端通信会自动带上该cookie
2、也可以储存在 localStorage,下次与服务端通信的时候从localstorage中取出来放到HTTP请求的头信息Authorization字段里面,或者放在POST请求的请求体里。
前者无法跨域(因为cookie是与域有关的),所以后者的方法更常用
1、用户首次登录时,服务器验证用户的凭证(如用户名和密码)。如果凭证有效,服务器将生成一个JWT,并将其发送回客户端。
2、客户端应用程序(如使用Vue.js、React等编写的前端代码)收到JWT后,会将其存储在本地存储(最常用的是localStorage
,有时也会使用sessionStorage
)中。这样做是为了持久化用户会话,即使在浏览器关闭后再重新打开时,用户仍然保持登录状态。
3、当客户端应用需要向服务器发送需要认证的请求时,它会从本地存储中检索JWT,并将其作为Authorization
头的一部分附加到请求中。通常,它是以Bearer
标记形式发送的,如Authorization: Bearer <token>
。
4、服务器在接收到请求时,会解析Authorization
头,验证JWT的有效性(如检查签名、是否过期等)。如果验证通过,则允许请求继续进行;如果失败,可能会返回错误响应,如401 Unauthorized。
注意:
1、每次客户端带着这个token来到服务端的时候,服务端用自己才有的secret,重写对内容做一次hash校验,与发来的jwt中的signatrue对应上了,就证明了没有被篡改过
2、有了jwt这种机制,服务端便不再需要存状态信息了
服务端当然还会用redis来当缓存,但redis的作用不是用来存放应用强依赖的会话信息,整个应用不是硬性依赖redis,单从会话保持这个点上来看是很大程度释放了集群的扩展能力
1、好处包括无状态认证和跨域认证支持,以及比较容易实现的分布式系统认证。
2、JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
3、此外将JWT存储在客户端(尤其是localStorage
)也并非十全十美的,依然存在泄露的风险,泄露了就完蛋,即也容易受到XSS(跨站脚本攻击)的攻击
XSS(跨站脚本攻击,Cross-Site Scripting)
XSS(跨站脚本攻击,Cross-Site Scripting)是一种在web应用中常见的安全漏洞。它允许攻击者将恶意脚本注入到其他用户浏览的页面中。简而言之,XSS攻击通常涉及一个攻击者向正常的网页插入恶意的HTML或JavaScript代码,当其他用户浏览那些被注入了恶意代码的网页时,恶意代码会在用户的浏览器上执行,从而允许攻击者绕过访问控制,如同获得了用户的权限。
XSS攻击主要可以分为以下三种类型:
-
反射型XSS(Reflected XSS):这种类型的XSS攻击通常发生在用户的输入被服务器接收后,立刻通过错误消息、搜索结果或其他响应直接反映到浏览器中。这需要用户主动点击了一个恶意链接或者提交表单才能触发。
-
存储型XSS(Stored XSS):存储型XSS较为严重。恶意脚本被存储在目标服务器上(比如数据库、消息论坛、访客留言),当其他用户访问含有恶意脚本的页面时,脚本会被执行。用户不需要进行任何特别的操作,访问网页即可触发。
-
基于DOM的XSS(DOM-based XSS):这种类型的XSS攻击是由于页面的脚本将来自用户的输入不安全地加入到页面的DOM中,导致当输入处理和插入到DOM时,没有正确的清理或转义,从而造成攻击。它主要发生在客户端,而不涉及到服务器端的数据处理。
简述一个XSS攻击的流程:
-
准备阶段:攻击者准备一个恶意脚本,目的可能是窃取cookie、会话令牌,重定向到恶意网站,或者盗取个人信息等。
-
注入阶段:攻击者通过某些手段(比如发送带有恶意链接的邮件、社交工程或将恶意代码注入到一个受欢迎的网站上)将恶意脚本传递给受害者。
-
执行阶段:无辜的用户不知情地执行了攻击者的恶意脚本,通常是通过点击链接或访问了某个含有恶意脚本的网页。如果是反射型或DOM-based XSS,这通常需要用户的交互;如果是存储型XSS,用户访问受影响的页面即可触发执行。
-
攻击成果:攻击者的脚本在受害者的浏览器上执行,实现攻击者的恶意目的,比如窃取用户的Session Cookies、展示欺诈内容、重定向到第三方站点等。
防范XSS攻击的关键在于对所有用户输入进行适当的处理,确保在输出时进行转义,不让任何未经验证的内容直接插入到HTML中,从而防止恶意脚本的执行。
jwt详解:https://egonlin.com/?p=9584
理论上讲,现在大多数新网站应用都会采用类似jwt的模式,而不是session的模式,但是针对一些旧的应用依然会采用session的存储方式
即将会话信息存放到服务端
所以我们还是来看一下这种方式
会话保持,即一个用户的请求被负载均衡无论分配到哪个web服务器上,会话信息依然存在,比如用户始终保持该有的登录状态
详解:
| 我们在访问网站的时候,进行登陆以后,服务器上回生成一个session,然后服务器会携带着session_id返回给浏览器记录一个cookie值,当第二次访问时,cookie会来服务器上与session进行对比,如果对比成功,则不需要重新登录 |
1、ip_hash:
负载均衡使用ip_hash算法,此时会话就存在各个web服务器的本地就行,一个用户对应自己唯一的ip,每次负载都打到同一台机器上
2、会话共享
把会话信息存放在一个所有web服务共享的存储空间里,这个共享的存储空间可以是
| 1、nfs共享存储(太慢):把多台后端服务器session文件目录挂载到NFS同一目录 |
| 2、mysql(也慢):通过程序将session存储到mysql数据库,需要程序本身支持 |
| 3、redis(块):通过程序将session存储到redis缓存,需要程序本身支持 |
| 找一台机器ip地址为192.168.71.16安装mysql, |
| 步骤见:https://egonlin.com/?p=9154 第1.3小节 |
2.0 关闭selinux、防火墙
| sed -ri 's/enforcing/disabled/g' /etc/sysconfig/selinux |
| systemctl stop firewalld |
| |
| setenforce 0 |
| iptables -t filter -F |
2.1、安装php-fpm
| 执行以下命令,安装epel源。 |
| yum -y install epel-release |
| |
| 执行以下命令,安装remi源。 |
| yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm |
| |
| 执行以下命令,安装Yum源管理工具。 |
| yum -y install yum-utils |
| |
| 执行以下命令,安装PHP 7.4。 |
| yum -y install php74-php-gd php74-php-pdo php74-php-mbstring php74-php-cli php74-php-fpm php74-php-mysqlnd |
| |
| 执行以下命令,验证PHP的安装版本。 |
| php74 -v |
| |
| 回显如下类似信息: |
| |
| PHP 7.4.33 (cli) (built: Jun 6 2023 15:55:08) ( NTS ) |
| Copyright (c) The PHP Group |
| Zend Engine v3.4.0, Copyright (c) Zend Technologies |
| |
| |
| cat /etc/opt/remi/php74/php-fpm.d/www.conf |grep -v '^;' |grep -v '^$' |
| |
| |
| systemctl start php74-php-fpm |
| |
| systemctl enable php74-php-fpm |
| systemctl status php74-php-fpm |
2.2、安装nginx对接php-fpm
| cat > /etc/nginx/conf.d/web.conf << "EOF" |
| server { |
| listen 8888; |
| server_name localhost; |
| |
| |
| |
| |
| location / { |
| root /usr/share/nginx/html; |
| index index.php index.html index.htm a.txt; |
| } |
| |
| |
| location ~ \.php$ { |
| fastcgi_pass 127.0.0.1:9000; |
| fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html$fastcgi_script_name; |
| include fastcgi_params; |
| } |
| } |
| EOF |
| |
| systemctl restart nginx |
补充
| 将 EOF 用引号括起来,这样 shell 就不会在 EOF 中的文本中进行变量替换或命令替换。 |
| cat > 文件 << "EOF" |
| This is a sample text with a dollar sign: $ |
| EOF |
2.3 部署应用程序
| 测试程序为phpmyadmin: 下载地址:https://www.phpmyadmin.net/downloads/ |
| **phpMyAdmin **是一个用PHP编写的软件工具,可以通过web方式控制和操作MySQL数据库。通过phpMyAdmin 可以完全对数据库进行操作,例如建立、复制和删除数据等等 |
| |
| wget https://files.phpmyadmin.net/phpMyAdmin/5.2.1/phpMyAdmin-5.2.1-all-languages.zip |
| unzip phpMyAdmin-5.2.1-all-languages.zip |
| |
| |
| mv phpMyAdmin-5.2.1-all-languages /usr/share/nginx/html/php |
| |
| |
| cd /usr/share/nginx/html/php |
| cp config.sample.inc.php config.inc.php |
| |
| vi config.inc.php |
| $cfg['Servers'][$i]['host'] = '192.168.71.16'; |
| |
| |
| chown -R nginx.nginx /usr/share/nginx/html/php |
测试两台web都可以正常访问
| [root@web03 ~] |
| user nginx; |
| worker_processes auto; |
| error_log /var/log/nginx/error.log; |
| pid /run/nginx.pid; |
| include /usr/share/nginx/modules/*.conf; |
| events { |
| worker_connections 1024; |
| } |
| http { |
| upstream webserver { |
| server 192.168.71.14:8888 max_fails=3 fail_timeout=5s; |
| server 192.168.71.15:8888 max_fails=3 fail_timeout=5s; |
| } |
| server { |
| listen 9090; |
| server_name 192.168.71.12; |
| location / { |
| proxy_pass http://webserver; |
| proxy_set_header X-Real-IP $remote_addr; |
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| proxy_set_header Host $http_host; |
| proxy_next_upstream error timeout http_500 http_502 http_503 http_504 http_403 http_404; |
| } |
| } |
| } |
通过负载均衡访问
phpMyAdmin就是管理数据库的,账号密码就是数据库里的账号密码,所以登录mysql创建
| create user 'root'@'192.168.71.%' identified by 'Egon@666'; |
| grant all on *.* to 'root'@'192.168.71.%'; |
| flush privileges; |
我们用负载均衡的地址http://192.168.71.12:9090/php登录,输入账号密码会报如下错误(因为涉及到会话共享的问题)
你直接用web服务器的地址http://192.168.71.15:8888/php/来登录是可以直接登录进去的,因为没有会话共享的问题,所有会话都存在这一台机器的本地
如果就采用默认的files来存储,那么我们首先就可以通过nfs共享存储来实现session共享
解放决方式一:挂载session文件的目录
| |
| yum -y install nfs* -y |
| [root@lb ~] |
| /data 192.168.71.0/24(rw,sync,no_root_squash) |
| |
| [root@lb ~] |
| [root@lb ~] |
| |
| |
| yum install nfs* -y |
| |
| |
| [root@web01 ~] |
| [root@web01 ~] |
| |
| [root@web02 ~] |
| [root@web02 ~] |
访问测试ok
找一台机器安装redis
vim /etc/redis/redis.conf
| 找到配置项 bind 127.0.0.1,然后将 127.0.0.1 修改为 0.0.0.0 或者你想让其绑定的具体IP,以允许远程访问。 |
| 找到protected-mode yes,并将其更改为 protected-mode no 这样可以允许非本地客户端连接。 |
| 增加设置密码:requirepass 123 |
重启redis,用客户端命令测试
| systemctl restart redis |
| |
| [root@lb ~] |
| Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. |
| 192.168.71.11:6379> keys * |
| (empty array) |
| 192.168.71.11:6379> set name egon |
| OK |
| 192.168.71.11:6379> get name |
| "egon" |
| 192.168.71.11:6379> keys * |
| 1) "name" |
| 192.168.71.11:6379> |
修改两个web服务器上的php配置文件
| [root@localhost session] |
| .............. |
| php_value[session.save_handler] = redis |
| php_value[session.save_path] = "tcp://192.168.71.11:6379?auth=123" |
| |
| ;php_value[session.save_path] = "tcp://192.168.2.11:6379" |
| ;php_value[session.save_handler] = files |
| ;php_value[session.save_path] = /var/opt/remi/php74/lib/php/session |
| .............. |
| |
| |
| |
| systemctl restart php74-php-fpm |
重新访问负载均衡:http://192.168.71.12:9090/php/报错
web服务器上没有为php环境安装php程序依赖的连接redis的模块
centos7.9、centos9通用安装方法
| systemctl list-unit-files|grep php |
| |
| wget http://pecl.php.net/get/redis-5.3.5.tgz |
| tar -zxvf redis-5.3.5.tgz |
| cd redis-5.3.5 |
| |
| yum -y install php74-php-devel |
| /opt/remi/php74/root/usr/bin/phpize |
| |
| |
| |
| /opt/remi/php74/root/usr/bin/php-config |
| |
| |
| ./configure |
| |
| |
| make |
| |
| [root@web01 ~/redis-5.3.5] |
| Installing shared extensions: /opt/remi/php74/root/usr/lib64/php/modules/ |
| [root@web01 ~/redis-5.3.5] |
| redis.so |
| |
| |
| [root@web01 ~/redis-5.3.5] |
| ;redis |
| extension=redis.so |
| |
| |
| systemctl restart php74-php-fpm |
| |
| [root@web01 ~/redis-5.3.5] |
| redis |
然后重新登录http://192.168.71.12:9090/php
发现session信息存入redis