Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
通過Cookie傳遞Session ID
setcookie(session_name(),session_id(),0,'/');
第一個參數中調用session_name()函數,返回當前session的名稱作為cookie的標識名稱,session名稱的默認值為PHPSESSID。
$_COOKIE[session_name()]等同于$_COOKIE["PHPSESSID"]
第二個參數中調用session_id()函數,返回當期session ID作為cookie的值
第三個參數的值設置為0時,是通過在php.ini文件中由session_cookie_lifetime選項設置的值,session_cookie_lifetime選項默認值為0,表示session ID將在客戶機的cookie中延續到瀏覽器關閉。
第四個參數"/",也是通過php配置文件制定的值,在php.ini中由session.cookie_path選項設置的值。默認為"/",表示在cookie中設置的路徑在整個域內都有效;
注意:當用戶禁用cookie后,服務器每次session_start()都會創建一個全新的session文件,其后果就是無法讓多個頁面php去共享一份session文件;
2 通過URL傳遞session ID
第一種方法:使用session_name()和session_id()函數傳遞
<?php
session_start();
echo '<a href="demp.php?'.session_name().'='.session_id().'">鏈接演示</a>'; //注意:引號使用 '" 先單引號,后雙引號
?>
<a href="index.php?sid=<?php echo session_id() ?>">首頁</a>
<form action="login.php?sid=<?php echo session_id() ?>" method="post">
注意:sid是自定義的變量,采用此種表單形式的:<form action="login.php?sid=<?php echo session_id() ?>" method="post">
表單接收過來的要給session_id()賦值,因為服務器不知道是使用哪個session_id;
if(isset($_GET["sid"])){
session_id($_GET["sid"]);
}
session_name() 是用來獲取或設置為當前會話的會話名稱,獲取會話名稱來自php.ini配置文件中session.name = "PHPSESSID"的默認值
session_id() 是用來獲取或設置為當前會話的會話ID
第二種方法:使用SID常量傳遞
此外,可以用常量 SID, 在會話啟動時被定義。
如果客戶端沒有發送適當的會話 cookie 的話, 則 SID 的格式為 session_name=session_id,否則就為一個空字符串。
因此可以無條件將其嵌入到 URL 中去。
案例1:
<?php
session_start(); //開啟Session
$_SESSION["username"]="admin"; //注冊一個Session變量,保存用戶名
echo "Session ID: ".session_id()."<br>"; //在當前頁面輸出Session ID
?>
<a href="test2.php?<?php echo SID ?>">通過URL傳遞Session ID</a> <!-- 在URL中附加SID -->
案例2:
Page1.php
<?php
Session_start(); //使用SESSION前必須調用該函數。
$_SESSION['name']="我是黑旋風李逵!"; //注冊一個SESSION變量
$_SESSION['passwd']="mynameislikui";
$_SESSION['time']=time();
echo '<br /><a href="page2.php">通過COOKIE傳遞SESSION</a>'; //如果客戶端支持cookie,可通過該鏈接傳遞session到下一頁。
echo '<br /><a href="page2.php?' . SID . '">通過URL傳遞SESSION</a>';//客戶端不支持cookie時,使用該辦法傳遞session.
?>
Page2.php
<?php
session_start();
echo $_SESSION['name']; //
echo $_SESSION['passwd']; //
echo date('Y m d H:i:s', $_SESSION['time']);
echo '<br /><a href="page1.php">返回山一頁</a>';
?>
<a href="nextpage.php?<?php echo strip_tags(SID); ?>">clickhere</a>.
用 strip_tags() 來輸出 SID 以避免 XSS 相關的攻擊。
3 修改配置文件php.ini
session.use_trans_sid 默認為 0(禁用)。
PHP 可以透明地自動轉換連接設置php.ini中的session.use_trans_sid = 1或者編譯時打開打開了--enable-trans-sid選項”
鏈接文件、header函數跳轉、表單跳轉,都可以添加session_name=session_id信息
唯一的javascript腳本中<script language="javascript">location.href='index.php'</script>不能添加,必須手工添加SID
文使用Spring Session實現了Spring Boot水平擴展,每個Spring Boot應用與其他水平擴展的Spring Boot一樣,都能處理用戶請求。如果宕機,Nginx會將請求反向代理到其他運行的Spring Boot應用上,如果系統需要增加吞吐量,只需要再啟動更多的Spring Boot應用即可。
本文選自《Spring Boot 2精髓:從構建小系統到架構分布式大系統》一書。
Spring Boot應用通常會部署在多個Web服務器上同時提供服務,這樣做有很多好處:
單個應用宕機不會停止服務,升級應用可以逐個升級而不必停止服務。
提高了應用整體的吞吐量。
我們稱這種部署方式為水平擴展,前端通過Nginx提供反向代理,會話管理可以通過Spring Session,使用Redis來存放Session。部署Spring Boot應用到任意一臺Web服務器上,從而提高了系統可靠性和可伸縮性。
當系統想提升處理能力的時候,通常用兩種選擇,一種是重置擴展架構,即提升現有系統硬件的處理能力,比如提高CPU頻率、使用更好的存儲器。另外一種選擇是水平擴展架構,即部署系統到更多的服務器上同時提供服務。這兩種方式各有利弊,現在通常都優先采用水平擴展架構,這是因為:
重置擴展架構
缺點:架構中的硬件提升能力有限,而且硬件能力提升往往需要更多的花銷;
優點:應用系統不需要做任何改變。
水平擴展
優點:成本便宜;
缺點:更多的應用導致管理更加復雜。對于Spring Boot 應用,會話管理是一個難點。
Spring Boot 應用水平擴展有兩個問題需要解決,一個是將用戶的請求派發到水平部署的任意一臺Spring Boot應用,通常用一個反向代理服務器來實現,本文將使用Nginx作為反向代理服務器。
反向代理(Reverse Proxy)方式是指接收internet上的連接請求,然后將請求轉發給內部網絡上的服務器,并將從服務器上得到的結果返回給internet上請求連接的客戶端,此時代理服務器對外就表現為一個反向代理服務器。
正向代理服務器:局域網內通過一個正向代理服務器訪問外網。
另外一個需要解決的問題是會話管理, 單個Spring Boot應用的會話由Tomcat來管理,會話信息與Tomcat存放在一起。如果部署多個Spring Boot應用,對于同一個用戶請求,即使請求通過Nginx派發到不同的Web服務器上,也能共享會話信息。有兩種方式可以實現。
復制會話:Web服務器通常都支持Session復制,一臺應用的會話信息改變將立刻復制到其他集群的Web服務器上。
集中式會話:所有Web服務器都共享一個會話,會話信息通常存放在一臺服務器上,本文使用Redis服務器來存放會話。
復制會話的缺點是每次會話改變需要復制到多臺Web服務器上,效率較低。因此Spring Boot應用采用第二種方式(集中式會話方式),結構如下圖所示。
上圖是一個大型分布式系統架構,包含了三個獨立的子系統。業務子系統一和業務子系統二分別部署在一臺Tomcat服務器上,業務子系統三部署在兩臺Tomcat服務器上,采用水平擴展。
架構采用Nginx作為反向代理,其后的各個子系統都采用Spring Session,將會話存放在Redis中,因此,這些子系統雖然是分開部署的,支持水平擴展,但能整合成一個大的系統。Nginx提供統一的入口,對于用戶訪問,將按照某種策略,比如根據訪問路徑派發到后面對應的Spring Boot應用中,Spring Boot調用Spring Session取得會話信息,Spring Session并沒有從本地存取會話,會話信息存放在Redis服務器上。
Nginx是一款輕量級的Web 服務器/反向代理服務器及電子郵件(IMAP/POP3)、TCP/UDP代理服務器,并在一個BSD-like協議下發行。由俄羅斯的程序設計師Igor Sysoev開發,供俄國大型的入口網站及搜索引擎Rambler使用。其特點是占有內存少,并發能力強,事實上Nginx的并發能力確實在同類型的網頁服務器中表現較好,國內使用Nginx的網站有百度、新浪、網易、騰訊等。
2.1 安裝Nginx
打開Nginx網站(http://nginx.org/),進入下載頁面,根據自己的操作系統選擇下載,以Windows系統為例,下載nginx/Windows-1.11.10版本,直接解壓,然后運行Nginx即可。
如果是Mac,可以運行:
>brew install nginx
Nginx默認會安裝在/usr/local/Cellar/nginx/目錄下,配置文件在/usr/local/etc/nginx/nginx.conf目錄下,日志文件在 /usr/local/var/log/nginx/目錄下。
以下是Nginx的常用命令:
nginx,啟動Nginx,默認監聽80端口。
nginx -s stop,快速停止服務器。
nginx -s quit,停止服務器,但要等到請求處理完畢后關閉。
nginx -s reload,重新加載配置文件。
Nginx啟動后,可以訪問http://127.0.0.1:80,會看到Nginx的歡迎頁面,如下圖所示。
如果80端口訪問不了,則可能是因為你下載的版本的原因,Nginx的HTTP端口配置成其他端口,編輯conf/nginx.conf,找到:
server {
listen 80;
}
修改listen參數到80端口即可。
Nginx的log目錄下提供了三個文件:
access.log,記錄了用戶的請求信息和響應。
error.log,記錄了Nginx運行的錯誤日志。
nginx.pid,包含了Nginx的進程號。
2.2 配置Nginx
Nginx的配置文件conf/nginx.conf下包含多個指令塊,我們主要關注http塊和location塊。
http塊:可以嵌套多個Server,配置代理、緩存、日志定義等絕大多數功能和第三方模塊,如mime-type定義、日志自定義、是否使用sendfile傳輸文件、連接超時時間、單連接請求數等。
location塊:配置請求的路由,以及各種頁面的處理情況。
由于本文主要是講水平擴展Spring Boot應用,因此,我們需要在http塊中增加upstream指令,內容如下:
http {
upstream backend { server 127.0.0.1:9000; server 127.0.0.1:9001
}
}
backend也可以為任意名字,我們在下面的配置將要引用到:
location / {
proxy_pass http://backend;
}
location后可以是一個正則表達式,我們這里用“/”表示所有客戶端請求都會傳給http:// backend,也就是我們配置的backend指令的地址列表。因此,整個http塊類似下面的樣子:
http {
include mime.types;
default_type application/octet-stream;
sendfile on; keepalive_timeout 65;
upstream backend {
server 127.0.0.1:9000;
server 127.0.0.1:9001;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://backend;
}
}
}
我們在后面將創建一個Spring Boot應用,并分別以9000和9001兩個端口啟動,然后在Spring Session的基礎上一步步來完成Spring Boot應用的水平擴展。
注意:Nginx反向代理默認情況下會輪詢后臺應用,還有一種配置是設置ip_hash,這樣,固定客戶端總是反向代理到后臺的某一個服務器。這種設置方式就不需要使用Spring Session來管理會話,使用Tomcat的會話管理即可。但弊端是如果服務器宕機或者因為維護重啟,則會話丟失。ip_hash設置如下:
upstream backend {
ip_hash; server 127.0.0.1:9000; server 127.0.0.1:9001
}
3.1 Spring Session介紹
在默認情況下,Spring Boot使用Tomcat服務器的Session實現,我們編寫一個例子用于測試:
@Controller
public class SpringSessionCrontroller {
Log log = LogFactory.getLog(SpringSessionCrontroller.class);
@RequestMapping("/putsession.html")
public @ResponseBody String putSession(HttpServletRequest request){
HttpSession session = request.getSession(); log.info(session.getClass()); log.info(session.getId()); String name = "xiandafu";
session.setAttribute("user", name);
return "hey,"+name;
}
}
如果訪問服務/putsession.html,控制臺輸出為:
SpringSessionCrontroller : class org.apache.catalina.session.StandardSessionFacade
SpringSessionCrontroller : F567C587EA25CBD5B9A75C62AB51904D
可以看到,Session管理是通過Tomcat提供的org.apache.catalina.session.StandardSessionFacade實現的。
在配置文件application.properties中添加如下內容:
spring.session.store-type=Redis|JDBC|Hazelcast|none
Spring Boot配置很容易切換到不同的Session管理方式,總共有以下幾種:
Redis,Session數據存放Redis中。
JDBC,會話數據存放在數據庫中,默認情況下SPRING_SESSION表存放Session基本信息,如sessionId、創建時間、最后一次訪問時間等,SPRING_SESSION_ ATTRIBUTES存放了session數據,ATTRIBUTE_NAME列保存了Session的Key,ATTRIBUTE_BYTES列以字節形式保存了Session的Value,Spring Session會自動創建這兩張表。
Hazelcast,Session數據存放到Hazelcast。
None,禁用Spring Session功能。
通過配置屬性spring.session.store-type來指定Session的存儲方式,如:
spring.session.store-type=Redis
修改為配置和增加Spring Session依賴后,如果訪問服務/putsession.html,控制臺輸出為:
SpringSessionCrontroller : class org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapperSpringSessionCrontroller : d4315e92-48e1-4a77-9819-f15df9361e68
可以看到,Session已經替換為HttpSessionWrapper實現,這個類負責Spring Boot 的Session存儲類型的具體實現。
3.2 使用Redis
本將用Redis來保存Session,你需要安裝Redis,如未安裝,請參考《Spring Boot 2精髓:從構建小系統到架構分布式大系統》中Redis一章,Spring Boot的配置如下:
spring.session.store-type=Redis
spring.redis.host=127.0.0.1spring.redis.port=6379
spring.redis.password=Redis!123
還需要引入對Redis的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
再次訪問/putsession.html后,我們通過Redis客戶端工具訪問Redis,比如使用redis-cli,輸入如下命令:
keys spring:session:*
查詢所有“spring:session:”開頭的keys,輸出如下:
3) "spring:session:sessions:expires:863c7e73-8249-4780-a08e-0ff2bdddda86"
...
7) "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86"
會話信息存放在“spring:session:sessions:”開頭的Key中,863c7e73-8249-4780-a08e-0ff2bdddda86代表一個會話id,“spring:session:sessions”是一個Hash數據結構,可以用Redis HASH相關的命令來查看這個用戶會話的數據,使用hgetall查看會話所有的信息:
>hgetall "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86"1) "sessionAttr:user"2) "maxInactiveInterval"
.......
使用以下命令來查看該Session的user信息:
>HMGET "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86" sessionAttr:user
sessionAttr:user是Spring Session存入Redis的Key值,sessionAttr:是其前綴,user是我們在Spring Boot中設置會話的Key。其他Spring Boot默認創建的Key還有:
creationTime,創建時間。
maxInactiveInterval,指定過期時間(秒)。
lastAccessedTime,上次訪問時間。
sessionAttr,以“sessionAttr:”為前綴的會話信息,比如sessionAttr: user。
因此,Spring Session使用Redis保存的會話將采用如下的Redis操作,類似如下:
>HMSET spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86 creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
注意:Spring Session的Redis實現并不是每次通過Session類獲取會話信息或者保存的時候都會調用Redis操作,它會先嘗試從內部的HashMap讀取值,如果沒有,才調用Redis的HMGET操作。同樣,當保存會話的時候,也沒有立即調用Redis操作,而是先保存到HashMap中,等待服務請求結束后再將變化的值使用HMSET更新。如果你想在保存會話操作后立即更新到Redis中,需要配置成IMMEDIATE模式,修改配置屬性:
spring.session.redis.flushMode=IMMEDIATE
我們注意到,還有另外一個Redis Key是“spring:session:sessions:expires:863c7e73-8249-4780- a08e-0ff2bdddda86”,這是因為Redis會話過期并沒有直接使用在session:sessions:key變量上,而是專門用在session:sessions:expires:key上,當此Key過期后,會自動清除對應的會話信息。使用ttl查看會話過期時間:
>ttl spring:session:sessions:expires:863c7e73-8249-4780-a08e-0ff2bdddda86(integer) 1469
默認是1800秒,即30分鐘,現在只剩下1469秒。
3.3 Nginx+Redis
在前文中,我們已經配置了:
upstream backend {
server 127.0.0.1:9000;
server 127.0.0.1:9001
}
假設在本機上部署了兩個Spring Boot應用,使用端口分別是9000和9001。進入工程目錄,運行mvn package,我們看到ch15.springsession\target\目錄下生成了ch17.springsession-0.0.1- SNAPSHOT.jar。然后進入命令行,進入target目錄,啟動這個Spring Boot應用:
java -jar target/ch15.springsession-0.0.1-SNAPSHOT.jar --server.port=9000
打開另外一個命令窗口,進入工程目錄,運行:
java -jar target/ch15.springsession-0.0.1-SNAPSHOT.jar --server.port=9001
這時候,我們就有兩臺Spring Boot應用。接下來,我們訪問以下地址,并刷新多次:
http://127.0.0.1/putsession.html
這時候就看到兩個Spring Boot應用均有日志輸出,比如9000端口的應用控制臺輸出如下:
class org.springframework.session.web.http.SessionRepositoryFilter....863c7e73-8249-4780-a08e-0ff2bdddda86
9001端口的Spring Boot應用也有類似輸出:
class org.springframework.session.web.http.SessionRepositoryFilter....863c7e73-8249-4780-a08e-0ff2bdddda86
我們看到,兩個Spring Boot應用都具有相同的sessionId,如果停掉任意一臺應用,系統還有另外一臺服務器提供服務,會話信息保存在Redis中。
內容豐富,涵蓋Spring Boot 2主流技術,作者有近20年的IT行業從業背景,資歷深厚。
作者:李家智
圖書鏈接:http://item.jd.com/12214143.html
上一節我們了解了網站登錄驗證和模擬登錄的基本原理。網站登錄驗證主要有兩種實現方式,一種是基于 Session + Cookies 的登錄驗證,另一種是基于 JWT 的登錄驗證。接下來兩節,我們就通過兩個實例來分別講解這兩種登錄驗證的分析和模擬登錄流程。
本節主要介紹 Session + Cookie 模擬登錄的流程。
在本節開始之前,我們需要先做好如下準備工作。
下面我們就用兩個案例來分別講解模擬登錄的實現。
本節有一個適用于 Session + Cookie 模擬登錄的案例網站,網址為:https://login2.scrape.center/,訪問之后,我們會看到一個登錄頁面,如圖所示:
我們輸入用戶名和密碼(用戶名和密碼都是 admin),然后點擊登錄。登錄成功后,我們便可以看到一個和之前案例類似的電影網站,如圖所示。
這個網站是基于傳統的 MVC 模式開發的,因此也比較適合 Session + Cookie 的模擬登錄。
對于這個網站,我們如果要模擬登錄,就需要先分析登錄過程究竟發生了什么。我們打開開發者工具,重新執行登錄操作,查看其登錄過程中發生的請求,如圖所示。
圖 10-5 登錄過程中發生的請求
從圖 10-5 中我們可以看到,在登錄的瞬間,瀏覽器發起了一個 POST 請求,目標 URL 為 https://login2.scrape.center/login,并通過表單提交的方式像服務器提交了登錄數據,其中包括 username 和 password 兩個字段,返回的狀態碼是 302,Response Headers 的 location 字段為根頁面,同時 Response Headers 還包含了 set-cookie 信息,設置了 Session ID。
由此我們可以發現,要實現模擬登錄,我們只需要模擬這個請求就好了。登錄完成后獲取 Response 設置的 Cookie,將它保存好,后續發出請求的時候帶上 Cookies 就可以正常訪問了。
好,那么我們就來用代碼實現一下吧!
在默認情況下,每次 requests 請求都是獨立且互不干擾的,比如我們第一次調用了 post 方法模擬登錄了一下,緊接著再調用 get 方法請求主頁面。其實這是兩個完全獨立的請求,第一次請求獲取的 Cookie 并不能傳給第二次請求,因此常規的順序調用是不能起到模擬登錄效果的。
我們來看一段無效的代碼:
import requests
from urllib.parse import urljoin
BASE_URL = 'https://login2.scrape.center/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'
response_login = requests.post(LOGIN_URL, data={
'username': USERNAME,
'password': PASSWORD
})
response_index = requests.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)
這里我們先定義了幾個基本的 URL 、用戶名和密碼,然后我們分別用 requests 請求了登錄的 URL 進行模擬登錄,緊接著請求了首頁來獲取頁面內容,能正常獲取數據嗎?由于 requests 可以自動處理重定向,我們可以在最后把 Response 的 URL 打印出來,如果它的結果是 INDEX_URL,那么證明模擬登錄成功并成功爬取到了首頁的內容。如果它跳回到了登錄頁面,那就說明模擬登錄失敗。
我們通過結果來驗證一下,運行結果如下:
Response Status 200
Response URL https://login2.scrape.center/login?next=/page/1
這里可以看到,其最終的頁面 URL 是登錄頁面的 URL。另外這里也可以通過 Response 的 text 屬性來驗證下頁面源碼,其源碼內容就是登錄頁面的源碼內容,由于內容較多,這里就不再輸出比對了。
總之,這個現象說明我們并沒有成功完成模擬登錄,這是因為 requests 直接調用 post、get 等方法,每次請求都是一個獨立的請求,都相當于是新開了一個瀏覽器打開這些鏈接,所以這兩次請求對應的 Session 并不是同一個,這里我們模擬了第一個 Session 登錄,并不能影響第二個 Session 的狀態,因此模擬登錄也就無效了。
那么怎樣才能實現正確的模擬登錄呢?
我們知道 Cookie 里面是保存了 Session ID 信息的,剛才也觀察到了登錄成功后 Response Headers 里面有 set-cookie 字段,實際上這就是讓瀏覽器生成了 Cookie。因為 Cookies 里面包含了 Session ID 的信息,所以只要后續的請求帶著這些 Cookie,服務器便能通過 Cookie 里的 Session ID 信息找到對應的 Session 了,因此,服務端對于這兩次請求就會使用同一個 Session 了。因為第一次我們已經成功完成了模擬登錄,所以 Session 里面就記錄了用戶的登錄信息,在第二次訪問的時候,由于是同一個 Session,服務器就能知道用戶當前是登錄狀態,那就能夠返回正確的結果而不再是跳轉到登錄頁面了。
所以,這里的關鍵在于兩次請求的 Cookie 的傳遞。這里我們可以把第一次模擬登錄后的 Cookie 保存下來,在第二次請求的時候加上這個 Cookie,代碼可以改寫如下:
import requests
from urllib.parse import urljoin
BASE_URL = 'https://login2.scrape.center/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'
response_login = requests.post(LOGIN_URL, data={
'username': USERNAME,
'password': PASSWORD
}, allow_redirects=False)
cookies = response_login.cookies
print('Cookies', cookies)
response_index = requests.get(INDEX_URL, cookies=cookies)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)
由于 requests 可以自動處理重定向,所以我們模擬登錄的過程要加上 allow_redirects 參數并將其設置為 False,使其不自動處理重定向。我們將登錄之后返回的 Response 賦值為 response_login,這樣調用 response_login 的 cookies 就是獲取了網站的 Cookie 信息了。這里 requests 自動幫我們解析了 Response Headers 的 set-cookie 字段并設置了 Cookie,所以我們不用再去手動解析 Response Headers 的內容了,直接使用 response_login 對象的 cookies 方法即可獲取 Cookie。
好,接下來我們再次用 requests 的 get 方法來請求網站的 INDEX_URL。不過這里和之前不同,get 方法增加了一個參數 cookies,這就是第一次模擬登錄完之后獲取的 Cookie,這樣第二次請求就能攜帶第一次模擬登錄獲取的 Cookie 信息了,此時網站會根據 Cookie 里面的 Session ID 信息查找到同一個 Session,校驗其已經是登錄狀態,然后返回正確的結果。
這里我們還是輸出最終的 URL,如果它是 INDEX_URL,就代表模擬登錄成功并獲取了有效數據,否則就代表模擬登錄失敗。
我們看下運行結果:
Cookies <RequestsCookieJar[<Cookie sessionid=psnu8ij69f0ltecd5wasccyzc6ud41tc for login2.scrape.center/>]>
Response Status 200
Response URL https://login2.scrape.center/page/1
這下沒有問題了,我們發現其 URL 就是 INDEX_URL,模擬登錄成功了!同時還可以進一步輸出 response_index 的 text 屬性看下是否獲取成功。
后續用同樣的方式爬取即可。但其實我們發現,這種實現方式比較煩瑣,每次還需要處理 Cookie 并一次傳遞,有沒有更簡便的方法呢?
有的,我們可以直接借助于 requests 內置的 Session 對象來幫我們自動處理 Cookie,使用了 Session 對象之后,requests 會自動保存每次請求后需要設置的 Cookie ,并在下次請求時自動攜帶它,就相當于幫我們維持了一個 Session 對象,這樣就更方便了。
所以,剛才的代碼可以簡化如下:
import requests
from urllib.parse import urljoin
BASE_URL = 'https://login2.scrape.center/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'
session = requests.Session()
response_login = session.post(LOGIN_URL, data={
'username': USERNAME,
'password': PASSWORD
})
cookies = session.cookies
print('Cookies', cookies)
response_index = session.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)
可以看到,這里我們無須再關心 Cookie 的處理和傳遞問題,我們聲明了一個 Session 對象,然后每次調用請求的時候都直接使用 Session 對象的 post 或 get 方法就好了。
運行效果是完全一樣的,結果如下:
Cookies <RequestsCookieJar[<Cookie sessionid=ssngkl4i7en9vm73bb36hxif05k10k13 for login2.scrape.center/>]>
Response Status 200
Response URL https://login2.scrape.center/page/1
因此,為了簡化寫法,這里建議直接使用 Session 對象進行請求,這樣我們無須關心 Cookie 的操作了,實現起來會更加方便。
這個案例整體來說比較簡單,但是如果碰上復雜一點的網站,如帶有驗證碼,帶有加密參數等,直接用 requests 并不好處理模擬登錄,如果登錄不了,那整個頁面不就都沒法爬取了嗎?有沒有其他的方式來解決這個問題呢?當然是有的,比如說我們可以使用 Selenium 來模擬瀏覽器,進而實現模擬登錄,然后獲取模擬登錄成功后的 Cookie,再把獲取的 Cookie 交由 requests 等來爬取就好了。
這里我們還是以剛才的頁面為例,把模擬登錄這塊交由 Selenium 來實現,后續的爬取交由 requests 來實現,相關的代碼如下:
from urllib.parse import urljoin
from selenium import webdriver
import requests
import time
BASE_URL = 'https://login2.scrape.center/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'
browser = webdriver.Chrome()
browser.get(BASE_URL)
browser.find_element_by_css_selector('input[name="username"]').send_keys(USERNAME)
browser.find_element_by_css_selector('input[name="password"]').send_keys(PASSWORD)
browser.find_element_by_css_selector('input[type="submit"]').click()
time.sleep(10)
# get cookies from selenium
cookies = browser.get_cookies()
print('Cookies', cookies)
browser.close()
# set cookies to requests
session = requests.Session()
for cookie in cookies:
session.cookies.set(cookie['name'], cookie['value'])
response_index = session.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)
這里我們使用 Selenium 先打開了 Chrome,然后跳轉到了登錄頁面,隨后模擬輸入了用戶名和密碼,接著點擊了登錄按鈕,我們可以發現瀏覽器提示登錄成功,然后跳轉到了主頁面。
這時候,我們通過調用 get_cookies 方法便能獲取當前瀏覽器所有的 Cookie,這就是模擬登錄成功之后的 Cookie,用這些 Cookie 我們就能訪問其他數據了。
接下來,我們聲明了 requests 的 Session 對象,然后遍歷了剛才的 Cookie 并將其設置到 Session 對象的 cookies 屬性上,接著再拿著這個 Session 對象去請求 INDEX_URL,就也能夠獲取對應的信息而不會跳轉到登錄頁面了。
運行結果如下:
Cookies [{'domain': 'login2.scrape.center', 'expiry': 1589043753.553155, 'httpOnly': True, 'name': 'sessionid', 'path': '/', 'sameSite': 'Lax', 'secure': False, 'value': 'rdag7ttjqhvazavpxjz31y0tmze81zur'}]
Response Status 200
Response URL https://login2.scrape.center/page/1
可以看到,這里的模擬登錄和后續的爬取也成功了。所以說,如果碰到難以模擬登錄的過程,我們也可以使用 Selenium 等模擬瀏覽器的操作方式來實現,其目的就是獲取登錄后的 Cookie,有了 Cookie 之后,我們再用這些 Cookie 爬取其他頁面就好了。
所以這里我們也可以發現,對于基于 Session + Cookie 驗證的網站,模擬登錄的核心要點就是獲取 Cookie。這個 Cookie 可以被保存下來或傳遞給其他的程序繼續使用,甚至可以將 Cookie 持久化存儲或傳輸給其他終端來使用。
另外,為了提高 Cookie 利用率或降低封號概率,可以搭建一個賬號池實現 Cookie 的隨機取用。
以上我們通過一個示例來演示了模擬登錄爬取的過程,以后遇到這種情形的時候就可以用類似的思路解決了。
本節代碼:https://github.com/Python3WebSpider/ScrapeLogin2。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。