第三方应用共享Laravel项目Session

  1. 1. 先看一下Laravel写的Session是什么
  2. 2. 设置Laravel Session配置,为共享准备
  3. 3. 明文 Cookie
  4. 4. 第三方应用兼容

Laravel 框架越来越被PHP开发者青睐,被应用得越来越广泛,大家都恨不得全部用它来重构项目,boss们当然不会同意,但是我们作为工程师也是不会放弃的,那要怎么办呢?

没错,按模块拆分重构,比如注册登入等小模块先重构。

一鼓作气,写完了发现Session不兼容,去Google百度了一下,也没找到什么好方案,没办法自己分析一下。

先看一下Laravel写的Session是什么

s:207:”a:4:{s:3:”abc”;i:1531800464;s:6:”_token”;s:40:”SPhvkWipry9tCxLrU431ueWE8iHBaMkOMU0acQV6”;s:9:”_previous”;a:1:{s:3:”url”;s:26:”http://yourdomain.com/";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}";

是不是很眼熟?没错,是PHP的序列化,要注意的是2次序列化。我们来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$sessData = <<<DATA
s:207:"a:4:{s:3:"abc";i:1531800464;s:6:"_token";s:40:"SPhvkWipry9tCxLrU431ueWE8iHBaMkOMU0acQV6";s:9:"_previous";a:1:{s:3:"url";s:26:"http://yourdomain.com/";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}";
DATA;

$sessData = unserialize(unserialize($sessData));
var_dump($sessData);
/**
array (
'abc' => 1531800527,
'_token' => 'SPhvkWipry9tCxLrU431ueWE8iHBaMkOMU0acQV6',
'_previous' =>
array (
'url' => 'http://yourdomain.com/',
),
'_flash' =>
array (
'old' =>
array (
),
'new' =>
array (
),
),
)
*/

设置Laravel Session配置,为共享准备

1
2
3
4
// config/session.php
'encrypt' => false,
'cookie' => 'PHPSESSID',
'domain' => 'yourdomain.com',

明文 Cookie

很多时候 Cookie 是需要被前端童鞋使用的,但是默认情况下 Laravel 在响应头中添加的 Cookie 信息是加密过的,类似 eyJpdiI6IjRwOFMyTkl2aGs2TGt4OUcxYXRNXC9BPT0iLCJ2YWx1ZSI6IkpHN0Fqb0ZSaDFxVHE0OHdFRXdXMHc9PSIsIm1hYyI6Ijc2MTljZDVmZDI1Mjg5MTk3NTBlZGM0MzUxMjUyZjQ5MzcxOGE1MWU4Y2ViZTBlYTY5YWRjZjNkZjUwNzNkMDEifQ%3D%3D,这种时候就得将那些需要 明文 传输的 Cookie 加入到 白名单 中去:

/app/Http/Middleware/EncryptCookies.php 中的 $except 数组中将其加入

1
2
3
protected $except = [
'PHPSESSID',
];

第三方应用兼容

思路如下:

读取

Redis取出(Laravel序列化数据,字符串)->unserialize * 2(得到数组)->SessionSerializer::encode()(得到序列化数据,字符串)->交给PHP内核处理

写入

PHP系列化数据->SessionSerializer::decode()(得到数组)->serialize() * 2->写入Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// PHP SESSION 序列化器
class SessionSerializer
{
public static function encode($array, $safe = true, $method = '')
{
$method = empty($method) ? ini_get("session.serialize_handler") : $method;
switch ($method) {
case "php":
return self::serializePhp($array, $safe);
break;
case "php_binary":
return self::serializePhpbinary($array, $safe);
break;
default:
throw new Exception("Unsupported session.serialize_handler: " . $method . ". Supported: php, php_binary");
}
}

public static function serializePhp($array, $safe = true)
{
if ($safe) {
$array = unserialize(serialize($array));
}
$raw = '';
$line = 0;
$keys = array_keys($array);
foreach ($keys as $key) {
$value = $array[$key];
$line++;
$raw .= $key . '|';
if (is_array($value) && isset($value['huge_recursion_blocker_we_hope'])) {
$raw .= 'R:' . $value['huge_recursion_blocker_we_hope'] . ';';
} else {
$raw .= serialize($value);
}
$array[$key] = ['huge_recursion_blocker_we_hope' => $line];
}
return $raw;
}

private static function serializePhpbinary($array, $safe = true)
{
return '';
}

public static function decode($session_data, $method = '')
{
$method = empty($method) ? ini_get("session.serialize_handler") : $method;
switch ($method) {
case "php":
return self::unserializePhp($session_data);
break;
case "php_binary":
return self::unserializePhpbinary($session_data);
break;
default:
throw new Exception("Unsupported session.serialize_handler: " . $method . ". Supported: php, php_binary");
}
}

private static function unserializePhp($session_data)
{
$return_data = [];
$offset = 0;
while ($offset < strlen($session_data)) {
if (!strstr(substr($session_data, $offset), "|")) {
throw new Exception("Invalid data, remaining: " . substr($session_data, $offset));
}
$pos = strpos($session_data, "|", $offset);
$num = $pos - $offset;
$varname = substr($session_data, $offset, $num);
$offset += $num + 1;
$data = unserialize(substr($session_data, $offset));
$return_data[$varname] = $data;
$offset += strlen(serialize($data));
}
return $return_data;
}

private static function unserializePhpbinary($session_data)
{
$return_data = [];
$offset = 0;
while ($offset < strlen($session_data)) {
$num = ord($session_data[$offset]);
$offset += 1;
$varname = substr($session_data, $offset, $num);
$offset += $num;
$data = unserialize(substr($session_data, $offset));
$return_data[$varname] = $data;
$offset += strlen(serialize($data));
}
return $return_data;
}
}

// Redis Session 驱动
class SessionRedis
{
protected $lifeTime = 900;
// Laravel项目中Session存储要求
protected $sessionName = 'your_laravel_app_name:';
// 共享cookie需要
protected $cookieDomain = 'yourdomain.com';
protected $handle = null;

public function open($savePath, $sessName)
{
$this->handle = new Redis;
$this->handle->connect('127.0.0.1', 6379);
return true;
}

public function close()
{
$this->gc(ini_get('session.gc_maxlifetime'));
$this->handle->close();
$this->handle = null;
return true;
}

public function read($sessID)
{
if (empty($sessID)) {
return;
}
// 从Redis读取
$sessData = $this->handle->get($this->sessionName . $sessID);
// 按Laravel格式反序列化
$sessData = unserialize(unserialize($sessData));
// 处理非法格式
$sessData = is_array($sessData) ? $sessData : [];
// 按PHP内核需要格式序列化
$sessData = SessionSerializer::encode($sessData);
// 注意这里需要返回的是序列化后的字符串
return $sessData;
}

public function write($sessID, $sessData)
{
if (empty($sessID)) {
return;
}
// 反序列化为数组
$sessData = SessionSerializer::decode($sessData);
// 按Laravel需要的格式序列化
$sessData = serialize(serialize($sessData));
// 存入Redis
return $this->handle->setex($this->sessionName . $sessID, $this->lifeTime, $sessData);
}

public function destroy($sessID)
{
if (empty($sessID)) {
return;
}

return $this->handle->delete($this->sessionName . $sessID);
}

public function gc($sessMaxLifeTime)
{
return true;
}

public function start()
{
ini_set('session.cookie_domain', isset($m[0]) ? $m[0] : $this->cookieDomain);
ini_set('session.auto_start', 0);
ini_set('session.use_trans_sid', 0);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1000);
ini_set('session.gc_maxlifetime', $this->lifeTime);
ini_set('session.use_cookies', 1);
ini_set('session.cookie_path', '/');
session_module_name('user');
session_set_save_handler(
array(&$this, 'open'),
array(&$this, 'close'),
array(&$this, 'read'),
array(&$this, 'write'),
array(&$this, 'destroy'),
array(&$this, 'gc')
);
session_start();
}
}

(new SessionRedis())->start();

最后,如果你有更好的方案,请留言给我,一起交流共同进步。