整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

PHP 如何传递Session ID

通过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服务器上,从而提高了系统可靠性和可伸缩性。

1 水平扩展实现

当系统想提升处理能力的时候,通常用两种选择,一种是重置扩展架构,即提升现有系统硬件的处理能力,比如提高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服务器上。

2 Nginx的安装和配置

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 Spring Session

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精髓:从构建小系统到架构分布式大系统》

内容丰富,涵盖Spring Boot 2主流技术,作者有近20年的IT行业从业背景,资历深厚。

作者:李家智

图书链接:http://item.jd.com/12214143.html


上一节我们了解了网站登录验证和模拟登录的基本原理。网站登录验证主要有两种实现方式,一种是基于 Session + Cookies 的登录验证,另一种是基于 JWT 的登录验证。接下来两节,我们就通过两个实例来分别讲解这两种登录验证的分析和模拟登录流程。

本节主要介绍 Session + Cookie 模拟登录的流程。

1. 准备工作

在本节开始之前,我们需要先做好如下准备工作。

  • 安装好了 requests 请求库并学会了其基本用法。
  • 安装好了 Selenium 库并学会了其基本用法。

下面我们就用两个案例来分别讲解模拟登录的实现。

2. 案例介绍

本节有一个适用于 Session + Cookie 模拟登录的案例网站,网址为:https://login2.scrape.center/,访问之后,我们会看到一个登录页面,如图所示:

我们输入用户名和密码(用户名和密码都是 admin),然后点击登录。登录成功后,我们便可以看到一个和之前案例类似的电影网站,如图所示。

这个网站是基于传统的 MVC 模式开发的,因此也比较适合 Session + Cookie 的模拟登录。

3. 模拟登录

对于这个网站,我们如果要模拟登录,就需要先分析登录过程究竟发生了什么。我们打开开发者工具,重新执行登录操作,查看其登录过程中发生的请求,如图所示。

图 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 的随机取用。

4. 总结

以上我们通过一个示例来演示了模拟登录爬取的过程,以后遇到这种情形的时候就可以用类似的思路解决了。

本节代码:https://github.com/Python3WebSpider/ScrapeLogin2。