CAS is currently a popular single sign-on protocol. The official provides the php version of client-side phpCAS. So far, So far, its coding style has remained in the PEAR era, without even using namespaces. Fortunately, phpCAS supports the introduction of composer, and I have done several Laravel project introductions without any problems. However, in the past two days, a project needs to be deployed from a single machine to a multi-machine deployment. I never expected to step on some pitfalls here. I will record them here. one time.
Callback pit
When jumping to the CAS Server for authentication, it was found that port 8080 was added to the incoming callback address. Because it is a multi-machine deployment, the access request will first pass through the load balancer (Alibaba Cloud SLB) and then reach the web server, and this 8080 is the listening port of the web server.
So I traced the logic of phpCAS to generate the callback address and found this piece of code:
if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { $server_port = $_SERVER['SERVER_PORT']; } else { $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']); $server_port = $ports[0]; }
And Alibaba Cloud's SLB will not be passed to the back-end serverX-FORWARDED-PORT
This http header, so phpCAS will get$_SERVER['SERVER_PORT']
which is the port 8080 of nginx.
Fortunately, phpCAS provides thesetFixedServiceURL
function, which allows us to manually set the callback address:
phpCAS::setFixedServiceURL($request->url());
The callback address is normal now, but the callback address is returned from the CAS Server. The client is informed that the ticket is invalid.
Continuing to check the logs and codes, I found that I was negligent here. When the CAS Server returned to the client, the url of the page washttp://client/login?ticket=xxxxx
, and When the client uses a ticket to exchange user information with the server, it also needs to bring the callback address (service) when applying for the ticket. The server will verify whether the ticket and service are consistent, and the service when applying for the ticket should behttp:/ /client/login
, so we need to remove the ticket parameter in the url.
phpCAS::setFixedServiceURL($this->getUrlWithoutTicket($request));
getUrlWithoutTicket
The function is as follows:
private function getUrlWithoutTicket(Request $request) { $query = parse_query($request->getQueryString()); unset($query['ticket']); $question = $request->getBaseUrl().$request->getPathInfo() == '/' ? '/?' : '?'; return $query ? $request->url().$question.http_build_query($query) : $request->url(); }
Session pit
This is a combination pit of phpCAS Laravel, which makes you lose your temper.
PHP defaults to session storage as a file, so a very important point when converting a single machine into multiple machines is to handle session sharing. The solution is also very simple, which is to change the Session storage method from file to redis/memecache/database, etc.
Laravel provides these drivers by default, so I excitedly changed the.env
file and changedSESSION_DRIVER
toredis
. I tried it online and found that it didn't work. The changes made by phpCAS to the$_SESSION
variable were not written to redis. What's going on!
So I followed Laravel's Session implementation and found that it was not the imagined use ofsession_set_save_handler
to register the Session read and write logic. In other words, Laravel's Session did not actually modify php's ## For the read and write logic of #$_SESSION, directly operate
$_SESSIONor follow the default behavior (read and write local files).
SessionHandlerInterfaceinterface. We can call it ourselves
session_set_save_handler:
session_set_save_handler(app(StartSession::class)->getSession($request)->getHandler());万I never expected an error!
session_write_close(): Session callback expects true/false return valueAfter chasing the Laravel code, I found that the
writemethod of the redis driver's parent class
Illuminate\Session\CacheBasedSessionHandlerreturns
void. So I submitted a PR to fix it, but I didn't expect it to be rejected. It turned out that someone had fixed it before and then reverted it, saying that it would cause the server to get stuck. However, I didn't find the specific issue.
session_write_closeNo error is reported, but there is still a problem with CAS login, and it keeps jumping between the CAS server and the callback url. So I chased all the logs and codes and found that the
destroymethod of the database driver class
Illuminate\Session\DatabaseSessionHandlerdid not remove
$this->exists## after destroying the Session. # The attribute is marked asfalse
, and phpCAS has a logic that isrenameSession
The consequence is$old_session = $_SESSION; session_destroy(); $session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket); session_id($session_id); session_start(); $_SESSION = $old_session;
The corresponding operation session The sql of the table executes update instead of insert, which means that the session data cannot be written to the session table!There is really no other way but to write a Session Wrapper yourself to handle it.
From the above two situations, the redis driver is easier to handle, as long as it can return true when calling the write method. So the code is as follows
namespace App\Services; use SessionHandlerInterface; class MySession implements SessionHandlerInterface { /** * @var SessionHandlerInterface */ protected $realHdl; /** * Session constructor. * @param SessionHandlerInterface $realHdl */ public function __construct(SessionHandlerInterface $realHdl) { $this->realHdl = $realHdl; } public function close() { return $this->realHdl->close(); } public function destroy($session_id) { return $this->realHdl->destroy($session_id); } public function gc($maxlifetime) { return $this->realHdl->gc($maxlifetime); } public function open($save_path, $name) { return $this->realHdl->open($save_path, $name); } public function read($session_id) { return $this->realHdl->read($session_id) ?: ''; } public function write($session_id, $session_data) { $this->realHdl->write($session_id, $session_data); return true; // 这里 } }
and then calls
session_set_save_handlerto become
Done!session_set_save_handler(new MySession(app(StartSession::class)->getSession($request)->getHandler()));