初始化
缺陷:主题配色需要优化整体的同风格
This commit is contained in:
@@ -36,22 +36,46 @@ class LoginService extends BaseNotAuthService
|
||||
$this->writeLoginLog(0, (string) $phone, '', 1, '用户或密码错误', $equipment, $browser);
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误!');
|
||||
}
|
||||
if (!password_verify($password, $userModel->password)) {
|
||||
// 老后台是无盐 sha1,没有明文无法预先转 bcrypt,所以 bcrypt 校验失败时再回落比对遗留密码
|
||||
$legacyHit = false;
|
||||
if (!password_verify($password, (string) $userModel->password)) {
|
||||
$legacyHit = $this->verifyLegacyPassword($userModel, (string) $password);
|
||||
if (!$legacyHit) {
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $phone,
|
||||
(string) $userModel->nick_name,
|
||||
1,
|
||||
'用户或密码错误',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误!');
|
||||
}
|
||||
}
|
||||
// 状态校验放在密码校验之后,避免未通过认证就泄露账号是否存在或被禁用
|
||||
if ((int) $userModel->status === 1) {
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $phone,
|
||||
(string) $userModel->nick_name,
|
||||
1,
|
||||
'用户或密码错误',
|
||||
'账号已被禁用',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误!');
|
||||
UtilsService::getInstance()->errorThrow('账号已被禁用,请联系管理员!');
|
||||
}
|
||||
$updateData = [
|
||||
'last_login_time' => get_time(),
|
||||
'updated_at' => get_time(),
|
||||
];
|
||||
if ($legacyHit) {
|
||||
// 命中遗留密码即刻升级成 bcrypt 并清空遗留列,下次登录走正常校验
|
||||
$updateData['password'] = password_hash($password, PASSWORD_DEFAULT);
|
||||
$updateData['legacy_password'] = '';
|
||||
$updateData['legacy_password_expire_at'] = 0;
|
||||
}
|
||||
if ($userModel->ip !== get_ip()) {
|
||||
$ipTable = json_decode($userModel->ip_table, true);
|
||||
if (!in_array(get_ip(), $ipTable ?? [])) {
|
||||
@@ -60,26 +84,14 @@ class LoginService extends BaseNotAuthService
|
||||
$updateData['ip'] = get_ip();
|
||||
$updateData['ip_table'] = json_encode($ipTable);
|
||||
}
|
||||
$update = AdminModel::where('id', $userModel->id)->update($updateData);
|
||||
if (!$update) {
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $phone,
|
||||
(string) $userModel->nick_name,
|
||||
1,
|
||||
'更新登录信息失败',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
UtilsService::getInstance()->errorThrow('更新失败!');
|
||||
}
|
||||
AdminModel::where('id', $userModel->id)->update($updateData);
|
||||
$userModel = AdminModel::with(['role:id,name,value'])->where('id', $userModel->id)->first();
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $userModel->phone,
|
||||
(string) $userModel->nick_name,
|
||||
0,
|
||||
'登录成功',
|
||||
$legacyHit ? '登录成功(遗留密码已升级)' : '登录成功',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
@@ -90,16 +102,99 @@ class LoginService extends BaseNotAuthService
|
||||
'avatar' => $userModel->avatar,
|
||||
'email' => $userModel->email,
|
||||
'role_id' => $userModel->role_id,
|
||||
'role_name' => $userModel->role->name,
|
||||
'role_value' => $userModel->role->value,
|
||||
// 迁移过来的账号可能 role_id=0,关联为空时不能直接取属性
|
||||
'role_name' => $userModel->role->name ?? '',
|
||||
'role_value' => $userModel->role->value ?? '',
|
||||
'ip' => $userModel->ip,
|
||||
'ip_table' => $userModel->ip_table,
|
||||
];
|
||||
$token = JWTService::getInstance()->generateToken($result);
|
||||
$result['token'] = $token;
|
||||
// 前端据此提示尽快改密:无盐 sha1 可被彩虹表秒破,升级后也建议换新密码
|
||||
$result['legacy_password_upgraded'] = $legacyHit;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录:删掉 Redis 会话,手里那张 token 立刻作废
|
||||
*
|
||||
* 注册在免登录组,token 缺失或已过期都不报错——前端登出时本地 token 常常已经清掉了,
|
||||
* 这时报错只会让用户卡在退出流程里。
|
||||
*/
|
||||
public function logout(): array
|
||||
{
|
||||
$decoded = JWTService::getInstance()->parseExpiringToken($this->refreshTtl());
|
||||
$userId = (int) ($decoded->data->id ?? 0);
|
||||
if ($userId > 0) {
|
||||
JWTService::getInstance()->revoke($userId);
|
||||
}
|
||||
return ['logout' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* 续签 token
|
||||
*
|
||||
* 前端拦截器在 401 时调这里。签名 + Redis 会话都要通过,只是放宽 exp,
|
||||
* 所以「被登出」和「过期太久」两种情况仍然要求重新登录。
|
||||
* @throws Exception
|
||||
*/
|
||||
public function refresh(): array
|
||||
{
|
||||
$decoded = JWTService::getInstance()->parseExpiringToken($this->refreshTtl());
|
||||
$data = isset($decoded->data) ? (array) $decoded->data : [];
|
||||
$userId = (int) ($data['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
UtilsService::getInstance()->notAuth('登录状态已失效,请重新登录');
|
||||
}
|
||||
// 会话还在才允许续签;顺带用库里的最新角色刷新 payload,改了角色不用等 token 过期
|
||||
$session = JWTService::getInstance()->getToken()->getUserInfo();
|
||||
$userModel = AdminModel::with(['role:id,name,value'])
|
||||
->where('id', $userId)
|
||||
->where('deleted_at', 0)
|
||||
->first();
|
||||
if (empty($userModel) || (int) $userModel->status === 1) {
|
||||
JWTService::getInstance()->revoke($userId);
|
||||
UtilsService::getInstance()->notAuth('账号不可用,请重新登录');
|
||||
}
|
||||
$payload = array_merge($session, [
|
||||
'id' => (int) $userModel->id,
|
||||
'phone' => $userModel->phone,
|
||||
'nick_name' => $userModel->nick_name,
|
||||
'avatar' => $userModel->avatar,
|
||||
'role_id' => (int) $userModel->role_id,
|
||||
'role_name' => $userModel->role->name ?? '',
|
||||
'role_value' => $userModel->role->value ?? '',
|
||||
]);
|
||||
return ['token' => JWTService::getInstance()->generateToken($payload)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 续签宽限期:token 过期后仍可续签的时长,超过就必须重新登录
|
||||
*/
|
||||
private function refreshTtl(): int
|
||||
{
|
||||
return (int) config('nl.jwt.refresh_ttl', 7 * 24 * 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 比对老系统的无盐 sha1 密码
|
||||
*
|
||||
* 只在 bcrypt 校验失败时调用。遗留列有失效时间,过期后一律走重置流程,
|
||||
* 因为无盐 sha1 可被彩虹表直接反查,不能无限期留着。
|
||||
*/
|
||||
private function verifyLegacyPassword(AdminModel $userModel, string $password): bool
|
||||
{
|
||||
$legacy = strtolower(trim((string) ($userModel->legacy_password ?? '')));
|
||||
if ($legacy === '') {
|
||||
return false;
|
||||
}
|
||||
$expireAt = (int) ($userModel->legacy_password_expire_at ?? 0);
|
||||
if ($expireAt > 0 && $expireAt < get_time()) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals($legacy, sha1($password));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册管理员账号
|
||||
*
|
||||
@@ -112,6 +207,10 @@ class LoginService extends BaseNotAuthService
|
||||
*/
|
||||
public function register($phone, $password, $email, $code): array
|
||||
{
|
||||
// 这是后台管理端,自助注册默认关闭:开着等于任何人都能给自己开一个管理员账号
|
||||
if (!config('nl.register.enabled', false)) {
|
||||
UtilsService::getInstance()->errorThrow('后台不开放自助注册,请联系管理员创建账号');
|
||||
}
|
||||
$userModel = AdminModel::where('phone', $phone)->where('deleted_at', 0)->first();
|
||||
if ($userModel) {
|
||||
UtilsService::getInstance()->errorThrow('账号已被占用!');
|
||||
@@ -122,9 +221,14 @@ class LoginService extends BaseNotAuthService
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'nick_name' => '新用户' . Str::random(),
|
||||
'avatar' => 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
'role_id' => 2,
|
||||
// 迁移的老角色统一偏移到 100 起,脚手架预留的 2(默认角色)不受影响,仍可作为默认值
|
||||
'role_id' => (int) config('nl.register.default_role_id', 2),
|
||||
// open_id 是 NOT NULL 且无默认值,不显式赋值会直接插入失败
|
||||
'open_id' => 'nl_' . bin2hex(random_bytes(15)),
|
||||
'ip' => get_ip(),
|
||||
'ip_table' => json_encode([get_ip()]),
|
||||
// status 列默认值是 1(禁用),不显式写成 0 新账号登录会被状态校验挡下
|
||||
'status' => 0,
|
||||
'created_at' => get_time(),
|
||||
]);
|
||||
if (!$createModel) {
|
||||
@@ -137,8 +241,8 @@ class LoginService extends BaseNotAuthService
|
||||
'nick_name' => $userInfo->nick_name,
|
||||
'avatar' => $userInfo->avatar,
|
||||
'email' => $userInfo->email ?? '',
|
||||
'role_name' => $userInfo->role->name,
|
||||
'role_value' => $userInfo->role->value,
|
||||
'role_name' => $userInfo->role->name ?? '',
|
||||
'role_value' => $userInfo->role->value ?? '',
|
||||
'ip' => $userInfo->ip,
|
||||
'ip_table' => $userInfo->ip_table,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user