概述
登录功能是对于每个动态系统来说都是非常基础的功能,用以区别用户身份、和对应的权限和信息,设计出一套安全的登录方案尤为重要,接下来我介绍一下常见的认证机制的登录设计方案。
方案设计
HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。系统登录的本质是确认用户的合法性和身份。
Cookie + Session 登录
在 B/S 系统中,登录功能通常都是基于 Cookie
来实现的。当用户登录成功后,一般会将登录状态记录到 Session
中。要实现服务端对客户端的登录信息进行验证都,需要在客户端保存一些信息(SessionId)
,并要求客户端在之后的每次请求中携带它们。在这样的场景下,使用 Cookie
无疑是最方便的,因此我们一般都会将 Session
的 Id 保存到 Cookie
中,当服务端收到请求后,通过验证 Cookie
中的信息来判断用户是否登录 。
用户首次登录流程
1、用户访问 www.stark.com/login
,并输入密码登录。
2、服务器验证密码无误后,会创建 SessionId
,并将它保存起来。
3、服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId
写入 Cookie
中。
cookice后续校验流程
获取cookice后续的访问就可以直接使用 Cookie 进行身份验证了
1、用户访问 www.stark.com/console
页面时,会自动带上第一次登录时写入的 Cookie
。
2、服务器端比对 Cookie
中的 SessionId
和保存在服务器端的 SessionId
是否一致。
3、如果一致,则身份验证成功,访问页面;如果无效,则需要用户重新登录。
需要注意的是: Cookie + Session
的方案中最关键的环节是传递Cookie
有时可能会面临Cookie
禁用的情况,记住只要把Cookie
的值传递给服务端得到SessionId
即可,可以是存储在LocalStorage
,也可以使用URL 的GET方式传输。
Cookie + Session 技术实现
Cookie + Session
的核心点在于数据的加密和解密的算法,在用户登录进行加密、生成Cookie
,在之后的交互的时候携带在header的信息头中。
加密函数代码:
php
function passportEncrypt($txt, $key = 'stark-server@2024@#$!'): string
{
$txt = 'yy-依加衣-' . time() . '-' . $txt;
srand((double)microtime() * 1000000);
$encrypt_key = md5(rand(0, 32000));
// 变量初始化
$ctr = 0;
$tmp = '';
for ($i = 0; $i < strlen($txt); $i++) {
$ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
$tmp .= $encrypt_key[$ctr] . ($txt[$i] ^ $encrypt_key[$ctr++]);
}
return base64_encode(passportKey($tmp, $key));
}
function passportKey($txt, $encrypt_key): string
{
$encrypt_key = md5($encrypt_key);
$ctr = 0;
$tmp = '';
for ($i = 0; $i < strlen($txt); $i++) {
$ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;
$tmp .= $txt[$i] ^ $encrypt_key[$ctr++];
}
return $tmp;
}
字符串解密函数:
php
function passportDecrypt($txt, $key = 'stark-server@2024@#$!')
{
$txt = str_replace(' ', '+', $txt);
$txt = passportKey(base64_decode($txt), $key);
$tmp = '';
for ($i = 0; $i < strlen($txt); $i++) {
if (!isset($txt[$i]) || !isset($txt[$i + 1])) {
return 0;
} else {
$tmp .= $txt[$i] ^ $txt[++$i];
}
}
$tmp = explode('-', $tmp);
$tmp[3] = $tmp[3] ?? 0;
return $tmp[3];
}
加密解密实现的具体逻辑:
php
//加密
$data = [
'admin_id' => $adminInfo['admin_id'],
'admin_name' => $adminInfo['admin_name'],
];
$demoStr = json_encode($data,JSON_UNESCAPED_UNICODE);
$authorization = passportEncrypt($demoStr);
Cookie::set('Auth-stark', $authorization,
['prefix' => 'think', 'expire' => 3600]
);
//解密
$json = passportDecrypt($authorization);
if(mb_strlen($json) > 0){
$demoData = json_decode($json,true);
}
Token 登录
由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId
,这样会导致服务器压力过大、无法避免 CSRF
攻击等缺点,我们可以使用 Token
的登录方式。
Token
是通过服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token
并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证,很多企业使用JWT
的技术来进行登录验证方式。
用户首次登录
1、用户访问 www.stark.com/login
,输入账号密码,并点击登录。
2、服务器端验证账号密码无误,创建 Token
。
3、服务器端将 Token
返回给客户端,由客户端存储在Header头信息里。
后续页面访问
1、用户访问 www.stark.com/login
时,带上第一次登录时获取的 Token
。
2、服务器端验证该 Token
,有效则身份验证成功,无效则踢回重新的登录。
Token 生成方式
最常见的 Token 生成方式是使用 JWT(Json Web Token)
,它是一种简洁的、自包含的方法,用于通信双方之间以 JSON 对象的形式安全的传递信息。
答案其实就在 Token 字符串中,其实 Token 并不是一串杂乱无章的字符串,而是通过多种算法拼接组合而成的字符串。
JWT 算法主要分为 3 个部分:header
(头信息),playload
(消息体),signature
(签名)。
header
部分指定了该 JWT 使用的签名算法;playload
部分表明了 JWT 的意图;signature
部分为 JWT 的签名,主要为了让JWT
不能被随意篡改。
JWT Token 技术实现
Compose 安装 Jwt 的两种方式,我使用的是6.10版本 :
shell
## 安装
composer require firebase/php-jwt 6.10
使用 composer.json 安装,加入文件,使用composer install
shell
"require": {
"firebase/php-jwt": "^6.10"
}
Jwt 主要是进行加密和解密,$payload
定义的是你需要存储的数组信息:
php
public static function encode(int $adminId = 0): string
{
$redis = new Redis(config('cache.stores.redis'));
$secretKey = Env::get("JWT.key"); // 获取JWT生成签名的密钥
$alg = Env::get("JWT.alg"); // 获取JWT加密算法
$payload = [
'admin_id' => $adminId, // 存储用户ID
'exp' => time() + Env::get("JWT.exp"), // 设定过期时间
];
$jwt = JWT::encode($payload, $secretKey, $alg); // 生成JWT令牌
$token = config('prefix.auth');
$redis->set($token.$adminId, $jwt,Env::get("JWT.exp") - rand(10,99));
return $jwt;
}
解密的逻辑:
php
public static function decode(string $AccessToken = ''){
$secretKey = Env::get("JWT.key"); // 获取JWT生成签名的密钥
$alg = Env::get("JWT.alg"); // 获取JWT加密算法
$secretKeyObj = new Key($secretKey,$alg);
$headers = new stdClass();
return JWT::decode($AccessToken, $secretKeyObj,$headers); // 使用JWT解密Token
}