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