diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9abc7da..4426da4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,6 +107,7 @@ jobs: MYSQL_DBNAME: ${{ secrets.MYSQL_DBNAME }} REDIS_HOST: ${{ secrets.REDIS_HOST }} REDIS_DBNAME: ${{ secrets.REDIS_DBNAME }} + ADMIN_PASSWD: ${{ secrets.ADMIN_PASSWD }} GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} GITHUB_CALLBACK_URL: ${{ secrets.OAUTH_GITHUB_CALLBACK_URL }} @@ -122,6 +123,7 @@ jobs: echo "MYSQL_DBNAME=${MYSQL_DBNAME:-techhaven}" echo "REDIS_HOST=${REDIS_HOST:-127.0.0.1:6379}" echo "REDIS_DBNAME=${REDIS_DBNAME:-blog}" + echo "ADMIN_PASSWD=${ADMIN_PASSWD:-}" echo "GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}" echo "GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}" echo "GITHUB_CALLBACK_URL=${GITHUB_CALLBACK_URL}" diff --git a/CONFIG.md b/CONFIG.md index 1cc7cb3..989c3f5 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -17,6 +17,9 @@ > `bin/conf/` 下只有 `.yml` 会被框架加载;`.tpl` / `.env` / `.md` 不会被加载。 +> **超级管理员引导**:仅当库中不存在 `admin.email` 对应的账号时才会创建管理员,初始密码取 +> `.env` 中的 `ADMIN_PASSWD`(无默认密码)。已存在的管理员密码不会被启动流程覆盖。 + ## 2. 切换环境 ```sh diff --git a/bin/conf/TechHaven.yml.tpl b/bin/conf/TechHaven.yml.tpl index 117897d..7e1aa85 100644 --- a/bin/conf/TechHaven.yml.tpl +++ b/bin/conf/TechHaven.yml.tpl @@ -6,10 +6,12 @@ ai: model: glm-4.7-flash api_key: # system default AI API key, used when user has not configured their own AI settings -# 默认超级管理员:服务启动时若不存在管理员则自动创建,用于首次登录并配置 SMTP 等系统设置 +# 默认超级管理员:服务启动时若该邮箱对应的管理员不存在则自动创建,用于首次登录并配置 SMTP 等系统设置。 +# 出于安全考虑不提供默认密码:首次部署必须在 .env 中显式设置 ADMIN_PASSWD, +# 已存在的管理员密码不会被启动流程覆盖。 admin: account: admin - passwd: admin123456 + passwd: ${ADMIN_PASSWD} email: admin@example.com # 分词字典配置 diff --git a/bin/conf/dev.env b/bin/conf/dev.env index 2b694f8..06fe005 100644 --- a/bin/conf/dev.env +++ b/bin/conf/dev.env @@ -10,6 +10,9 @@ REDIS_DBNAME=blog_dev REDIS_HOST=127.0.0.1:6379 DATA_PATH="" +# 开发环境超级管理员初始密码(仅首次创建管理员时使用) +ADMIN_PASSWD=admin123456 + # GitHub OAuth 登录(开发环境) # 在 https://github.com/settings/developers 新建 OAuth App 后填入 Client ID / Secret # callback_url 必须与 OAuth App 的 Authorization callback URL 完全一致 diff --git a/bin/conf/prod.env.example b/bin/conf/prod.env.example index 1c554e6..c8a5a7c 100644 --- a/bin/conf/prod.env.example +++ b/bin/conf/prod.env.example @@ -9,6 +9,10 @@ MYSQL_DBNAME=techhaven REDIS_HOST=127.0.0.1:6379 REDIS_DBNAME=blog +# 首次部署的超级管理员初始密码(仅当库中不存在 admin.email 对应账号时使用) +# 请填写强密码;已存在的管理员密码不会被覆盖 +ADMIN_PASSWD= + # GitHub OAuth 登录(生产环境) GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= diff --git a/src/manager/user_manager.cc b/src/manager/user_manager.cc index efee48f..cbd7ba2 100644 --- a/src/manager/user_manager.cc +++ b/src/manager/user_manager.cc @@ -71,7 +71,7 @@ static void setUserCache(const data::UserInfo::ptr& info) static chen::ConfigVar::ptr g_admin_account = chen::Config::Lookup("admin.account", std::string("admin"), "default super admin account"); static chen::ConfigVar::ptr g_admin_passwd = - chen::Config::Lookup("admin.passwd", std::string("admin123456"), "default super admin password"); + chen::Config::Lookup("admin.passwd", std::string(""), "bootstrap super admin password (required only on first seed)"); static chen::ConfigVar::ptr g_admin_email = chen::Config::Lookup("admin.email", std::string("admin@example.com"), "default super admin email"); @@ -206,18 +206,31 @@ void UserManager::ensureSuperAdmin() std::string account = g_admin_account->getValue(); std::string passwd = g_admin_passwd->getValue(); std::string email = g_admin_email->getValue(); - if (account.empty() || passwd.empty() || email.empty()) { - ERROR(logger) << "admin account or password empty, skip seeding super admin"; + if (email.empty()) { + ERROR(logger) << "admin.email empty, skip seeding super admin"; return; } auto info = data::UserInfoDao::QueryByEmail(email, db); + bool is_new = false; if (!info) { + // 仅在首次创建时要求配置初始密码,已有管理员不会被重置密码 + if (account.empty() || passwd.empty()) { + ERROR(logger) << "super admin not found and admin.account/admin.passwd empty, " + << "please configure them to bootstrap the first admin"; + return; + } + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + ERROR(logger) << "hash super admin password failed, skip seeding"; + return; + } info.reset(new data::UserInfo); - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + info->setPasswd(passwd_hash); info->setName(account); info->setAccount(account); info->setEmail(email); + is_new = true; } info->setRole(Role::ADMIN); @@ -230,7 +243,11 @@ void UserManager::ensureSuperAdmin() } setUserCache(info); - INFO(logger) << "seeded super admin account=" << account << " email=" << email; + if (is_new) { + INFO(logger) << "seeded super admin account=" << account << " email=" << email; + } else { + INFO(logger) << "super admin ensured account=" << info->getAccount() << " email=" << email; + } } blog::data::UserInfo::ptr UserManager::getByName(const std::string& v) diff --git a/src/servlets/user/user_admin_create_servlet.cc b/src/servlets/user/user_admin_create_servlet.cc index 6b309f3..e494ac9 100644 --- a/src/servlets/user/user_admin_create_servlet.cc +++ b/src/servlets/user/user_admin_create_servlet.cc @@ -11,7 +11,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserAdminCreateServlet::UserAdminCreateServlet() - : BlogLoginedServlet("UserAdminCreateServlet") + : BlogAdminServlet("UserAdminCreateServlet") { } @@ -28,23 +28,6 @@ int32_t UserAdminCreateServlet::handle( DEFINE_AND_CHECK_TYPE(result, int32_t, role, "role"); DEFINE_AND_CHECK_TYPE(result, int32_t, state, "state"); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t user_role = current_user->getRole(); - - if (user_role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - if (account.empty() || passwd.empty()) { result->setErrno(errcode::PARAM_MISSING, "account and password required"); break; @@ -83,11 +66,17 @@ int32_t UserAdminCreateServlet::handle( result->setErrno(errcode::DB_CONNECTION_FAILED); break; } + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + chen::ITransaction::ptr trans = db->openTransaction(); data::UserInfo::ptr info(new data::UserInfo); info->setAccount(account); info->setEmail(email); - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + info->setPasswd(passwd_hash); info->setRole(role); info->setState(state); info->setName(account); diff --git a/src/servlets/user/user_admin_create_servlet.h b/src/servlets/user/user_admin_create_servlet.h index 25e0e95..17f26cc 100644 --- a/src/servlets/user/user_admin_create_servlet.h +++ b/src/servlets/user/user_admin_create_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminCreateServlet : public BlogLoginedServlet { +class UserAdminCreateServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminCreateServlet(); diff --git a/src/servlets/user/user_admin_delete_servlet.cc b/src/servlets/user/user_admin_delete_servlet.cc index 9100dff..3a1a8fa 100644 --- a/src/servlets/user/user_admin_delete_servlet.cc +++ b/src/servlets/user/user_admin_delete_servlet.cc @@ -12,7 +12,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserAdminDeleteServlet::UserAdminDeleteServlet() - : BlogLoginedServlet("UserAdminDeleteServlet") + : BlogAdminServlet("UserAdminDeleteServlet") { } @@ -25,23 +25,6 @@ int32_t UserAdminDeleteServlet::handle( do { DEFINE_AND_CHECK_TYPE(result, int64_t, user_id, "user_id"); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - auto info = UserMgr::GetInstance()->get(user_id); if (!info) { result->setErrno(errcode::USER_NOT_FOUND); diff --git a/src/servlets/user/user_admin_delete_servlet.h b/src/servlets/user/user_admin_delete_servlet.h index 4bccaa8..dc3e112 100644 --- a/src/servlets/user/user_admin_delete_servlet.h +++ b/src/servlets/user/user_admin_delete_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminDeleteServlet : public BlogLoginedServlet { +class UserAdminDeleteServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminDeleteServlet(); diff --git a/src/servlets/user/user_admin_detail_servlet.cc b/src/servlets/user/user_admin_detail_servlet.cc index 7b6194b..ba6341c 100644 --- a/src/servlets/user/user_admin_detail_servlet.cc +++ b/src/servlets/user/user_admin_detail_servlet.cc @@ -9,7 +9,7 @@ namespace blog { namespace servlet { UserAdminDetailServlet::UserAdminDetailServlet() - : BlogLoginedServlet("UserAdminDetailServlet") + : BlogAdminServlet("UserAdminDetailServlet") { } @@ -22,23 +22,6 @@ int32_t UserAdminDetailServlet::handle( do { DEFINE_AND_CHECK_TYPE(result, int64_t, user_id, "user_id"); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - auto info = UserMgr::GetInstance()->get(user_id); if (!info) { result->setErrno(errcode::USER_NOT_FOUND); diff --git a/src/servlets/user/user_admin_detail_servlet.h b/src/servlets/user/user_admin_detail_servlet.h index 6354ac4..beba9ad 100644 --- a/src/servlets/user/user_admin_detail_servlet.h +++ b/src/servlets/user/user_admin_detail_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminDetailServlet : public BlogLoginedServlet { +class UserAdminDetailServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminDetailServlet(); diff --git a/src/servlets/user/user_admin_lists_servlet.cc b/src/servlets/user/user_admin_lists_servlet.cc index 3c6ab1d..4c063d7 100644 --- a/src/servlets/user/user_admin_lists_servlet.cc +++ b/src/servlets/user/user_admin_lists_servlet.cc @@ -16,7 +16,7 @@ namespace servlet { static chen::Logger::ptr g_logger = LOG_ROOT(); UserAdminListsServlet::UserAdminListsServlet() - : BlogLoginedServlet("UserAdminListsServlet") + : BlogAdminServlet("UserAdminListsServlet") { } @@ -33,23 +33,6 @@ int32_t UserAdminListsServlet::handle( int32_t state = request->getParamAs("state", -1); int32_t days = request->getParamAs("days", 0); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - uint64_t offset = (page_num - 1) * page_size; std::vector users; uint64_t total = UserMgr::GetInstance()->listByPages(users, offset, page_size, rrole, state, days, true); diff --git a/src/servlets/user/user_admin_lists_servlet.h b/src/servlets/user/user_admin_lists_servlet.h index ca27d55..c9ec609 100644 --- a/src/servlets/user/user_admin_lists_servlet.h +++ b/src/servlets/user/user_admin_lists_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminListsServlet : public BlogLoginedServlet { +class UserAdminListsServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminListsServlet(); diff --git a/src/servlets/user/user_admin_recover_servlet.cc b/src/servlets/user/user_admin_recover_servlet.cc index 2c4e39f..0047efd 100644 --- a/src/servlets/user/user_admin_recover_servlet.cc +++ b/src/servlets/user/user_admin_recover_servlet.cc @@ -14,7 +14,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserAdminRecoverServlet::UserAdminRecoverServlet() - : BlogLoginedServlet("UserAdminRecoverServlet") + : BlogAdminServlet("UserAdminRecoverServlet") { } @@ -33,23 +33,6 @@ int32_t UserAdminRecoverServlet::handle( user_ids.insert(chen::TypeUtil::Atoi(i)); } - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - std::vector infos; for (const int64_t& id : user_ids) { auto info = UserMgr::GetInstance()->get(id); diff --git a/src/servlets/user/user_admin_recover_servlet.h b/src/servlets/user/user_admin_recover_servlet.h index b2d9216..dd89c10 100644 --- a/src/servlets/user/user_admin_recover_servlet.h +++ b/src/servlets/user/user_admin_recover_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminRecoverServlet : public BlogLoginedServlet { +class UserAdminRecoverServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminRecoverServlet(); diff --git a/src/servlets/user/user_admin_reset_passwd_servlet.cc b/src/servlets/user/user_admin_reset_passwd_servlet.cc index b619fcf..492bee3 100644 --- a/src/servlets/user/user_admin_reset_passwd_servlet.cc +++ b/src/servlets/user/user_admin_reset_passwd_servlet.cc @@ -12,7 +12,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserAdminResetPasswdServlet::UserAdminResetPasswdServlet() - : BlogLoginedServlet("UserAdminResetPasswdServlet") + : BlogAdminServlet("UserAdminResetPasswdServlet") { } @@ -27,23 +27,6 @@ int32_t UserAdminResetPasswdServlet::handle( DEFINE_AND_CHECK_STRING(result, passwd_f, "passwd_f"); DEFINE_AND_CHECK_STRING(result, passwd_s, "passwd_s"); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - if (passwd_f.empty() || passwd_s.empty()) { result->setErrno(errcode::PARAM_MISSING, "password required"); break; @@ -60,13 +43,19 @@ int32_t UserAdminResetPasswdServlet::handle( break; } + std::string passwd_hash = HashPassword(passwd_s); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + chen::ITransaction::ptr trans = db->openTransaction(); data::UserInfo::ptr info = UserMgr::GetInstance()->get(id); if (!info) { result->setErrno(errcode::USER_NOT_FOUND); break; } - info->setPasswd(chen::EncryptorUtil::MD5(passwd_s)); + info->setPasswd(passwd_hash); if (data::UserInfoDao::Update(info, db)) { result->setErrno(errcode::DB_OPERATION_FAILED, "insert user failed"); diff --git a/src/servlets/user/user_admin_reset_passwd_servlet.h b/src/servlets/user/user_admin_reset_passwd_servlet.h index b0d19b8..cb82731 100644 --- a/src/servlets/user/user_admin_reset_passwd_servlet.h +++ b/src/servlets/user/user_admin_reset_passwd_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminResetPasswdServlet : public BlogLoginedServlet { +class UserAdminResetPasswdServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminResetPasswdServlet(); diff --git a/src/servlets/user/user_admin_stats_servlet.cc b/src/servlets/user/user_admin_stats_servlet.cc index 22b1331..40403b4 100644 --- a/src/servlets/user/user_admin_stats_servlet.cc +++ b/src/servlets/user/user_admin_stats_servlet.cc @@ -6,7 +6,7 @@ namespace blog { namespace servlet { UserAdminStatsServlet::UserAdminStatsServlet() - : BlogLoginedServlet("UserAdminStatsServlet") + : BlogAdminServlet("UserAdminStatsServlet") { } @@ -17,23 +17,6 @@ int32_t UserAdminStatsServlet::handle( Result::ptr result) { do { - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - std::vector ids; UserMgr::GetInstance()->getAllIds(ids, false); diff --git a/src/servlets/user/user_admin_stats_servlet.h b/src/servlets/user/user_admin_stats_servlet.h index c7a67e2..9c5856f 100644 --- a/src/servlets/user/user_admin_stats_servlet.h +++ b/src/servlets/user/user_admin_stats_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminStatsServlet : public BlogLoginedServlet { +class UserAdminStatsServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminStatsServlet(); diff --git a/src/servlets/user/user_admin_update_servlet.cc b/src/servlets/user/user_admin_update_servlet.cc index 7ca6daa..b7b8965 100644 --- a/src/servlets/user/user_admin_update_servlet.cc +++ b/src/servlets/user/user_admin_update_servlet.cc @@ -11,7 +11,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserAdminUpdateServlet::UserAdminUpdateServlet() - : BlogLoginedServlet("UserAdminUpdateServlet") + : BlogAdminServlet("UserAdminUpdateServlet") { } @@ -24,23 +24,6 @@ int32_t UserAdminUpdateServlet::handle( do { DEFINE_AND_CHECK_TYPE(result, int64_t, user_id, "user_id"); - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t user_role = current_user->getRole(); - - if (user_role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - auto info = UserMgr::GetInstance()->get(user_id); if (!info) { result->setErrno(errcode::USER_NOT_FOUND); @@ -89,7 +72,12 @@ int32_t UserAdminUpdateServlet::handle( result->setErrno(errcode::USER_INVALID_PASSWORD); break; } - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + info->setPasswd(passwd_hash); } if (has_role) { if (role < 1 || role > 4) { diff --git a/src/servlets/user/user_admin_update_servlet.h b/src/servlets/user/user_admin_update_servlet.h index 3cf43db..537e348 100644 --- a/src/servlets/user/user_admin_update_servlet.h +++ b/src/servlets/user/user_admin_update_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserAdminUpdateServlet : public BlogLoginedServlet { +class UserAdminUpdateServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserAdminUpdateServlet(); diff --git a/src/servlets/user/user_create_servlet.cc b/src/servlets/user/user_create_servlet.cc index a1bd197..09522b7 100644 --- a/src/servlets/user/user_create_servlet.cc +++ b/src/servlets/user/user_create_servlet.cc @@ -73,12 +73,18 @@ int32_t UserCreateServlet::handle( } } + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + // 开启事务 chen::ITransaction::ptr trans = db->openTransaction(); data::UserInfo::ptr info(new data::UserInfo); info->setAccount(account); info->setEmail(email); - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + info->setPasswd(passwd_hash); info->setState(UserManager::Status::ACTIVE); info->setName(account); diff --git a/src/servlets/user/user_list_servlet.cc b/src/servlets/user/user_list_servlet.cc index ad6e715..eca1ee9 100644 --- a/src/servlets/user/user_list_servlet.cc +++ b/src/servlets/user/user_list_servlet.cc @@ -12,7 +12,7 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); UserListServlet::UserListServlet() - : BlogLoginedServlet("UserListServlet") + : BlogAdminServlet("UserListServlet") { } @@ -23,23 +23,6 @@ int32_t UserListServlet::handle( Result::ptr result) { do { - int64_t uid = getUserId(request); - if (!uid) { - result->setErrno(errcode::NOT_LOGIN); - break; - } - auto current_user = UserMgr::GetInstance()->get(uid); - if (!current_user) { - result->setErrno(errcode::USER_NOT_FOUND); - break; - } - int32_t role = current_user->getRole(); - - if (role != UserManager::Role::ADMIN) { - result->setErrno(errcode::ACCESS_DENIED); - break; - } - std::vector ids; UserMgr::GetInstance()->getAllIds(ids, false); diff --git a/src/servlets/user/user_list_servlet.h b/src/servlets/user/user_list_servlet.h index fcf86c2..b430a68 100644 --- a/src/servlets/user/user_list_servlet.h +++ b/src/servlets/user/user_list_servlet.h @@ -5,7 +5,7 @@ namespace blog { namespace servlet { -class UserListServlet : public BlogLoginedServlet { +class UserListServlet : public BlogAdminServlet { public: typedef std::shared_ptr ptr; UserListServlet(); diff --git a/src/servlets/user/user_login_servlet.cc b/src/servlets/user/user_login_servlet.cc index bdc5ca7..de7cd71 100644 --- a/src/servlets/user/user_login_servlet.cc +++ b/src/servlets/user/user_login_servlet.cc @@ -57,7 +57,7 @@ int32_t UserLoginServlet::handle( result->setErrno(errcode::AUTH_CODE_INVALID); break; } - if (info->getPasswd() != chen::EncryptorUtil::MD5(passwd)) { + if (!VerifyPassword(passwd, info->getPasswd())) { result->setErrno(errcode::USER_PASSWORD_WRONG); break; } @@ -67,6 +67,22 @@ int32_t UserLoginServlet::handle( break; } + // 历史 MD5 哈希在登录成功后透明升级为 bcrypt;失败不影响本次登录,下次登录重试 + if (IsLegacyPasswordHash(info->getPasswd())) { + std::string passwd_hash = HashPassword(passwd); + auto upgrade_db = getDB(); + if (!passwd_hash.empty() && upgrade_db) { + std::string old_hash = info->getPasswd(); + info->setPasswd(passwd_hash); + if (data::UserInfoDao::Update(info, upgrade_db) == 0) { + UserMgr::GetInstance()->update(info); + } else { + info->setPasswd(old_hash); + ERROR(logger) << "upgrade password hash failed user=" << info->getId(); + } + } + } + // 2FA: 密码验证通过后,如果启用了TOTP,返回临时凭证等待第二步验证 if (info->getTotpEnabled()) { std::string tempToken = UserManager::generateToken(); diff --git a/src/servlets/user/user_oauth_github_callback_servlet.cc b/src/servlets/user/user_oauth_github_callback_servlet.cc index 9f2a30a..a4b916d 100644 --- a/src/servlets/user/user_oauth_github_callback_servlet.cc +++ b/src/servlets/user/user_oauth_github_callback_servlet.cc @@ -389,7 +389,7 @@ int32_t UserOauthGithubCallbackServlet::handle( user->setGithub("https://github.com/" + profile.login); } // GitHub 用户无本地密码,写入不可用的随机值 - user->setPasswd(chen::EncryptorUtil::MD5(UserManager::generateToken())); + user->setPasswd(HashPassword(UserManager::generateToken())); user->setRole(UserManager::Role::USER); user->setState(UserManager::Status::ACTIVE); user->setCreateTime(time(0)); diff --git a/src/servlets/user/user_reset_passwd_servlet.cc b/src/servlets/user/user_reset_passwd_servlet.cc index ad6199e..03e18b7 100644 --- a/src/servlets/user/user_reset_passwd_servlet.cc +++ b/src/servlets/user/user_reset_passwd_servlet.cc @@ -55,9 +55,19 @@ int32_t UserResetPasswdServlet::handle( break; } + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + chen::ITransaction::ptr trans = db->openTransaction(); data::UserInfo::ptr info = UserMgr::GetInstance()->getByEmail(email); - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + if (!info) { + result->setErrno(errcode::USER_EMAIL_NOT_REGISTER); + break; + } + info->setPasswd(passwd_hash); if (data::UserInfoDao::Update(info, db)) { result->setErrno(errcode::DB_OPERATION_FAILED, "insert user failed"); diff --git a/src/servlets/user/user_update_servlet.cc b/src/servlets/user/user_update_servlet.cc index 97cf44a..8e76b05 100644 --- a/src/servlets/user/user_update_servlet.cc +++ b/src/servlets/user/user_update_servlet.cc @@ -59,7 +59,7 @@ int32_t UserUpdateServlet::handle( result->setErrno(errcode::PARAM_MISSING, "old password required"); break; } - if (info->getPasswd() != chen::EncryptorUtil::MD5(old_passwd)) { + if (!VerifyPassword(old_passwd, info->getPasswd())) { result->setErrno(errcode::USER_OLD_PASSWORD_WRONG); break; } @@ -69,7 +69,12 @@ int32_t UserUpdateServlet::handle( info->setName(name); } if (!passwd.empty()) { - info->setPasswd(chen::EncryptorUtil::MD5(passwd)); + std::string passwd_hash = HashPassword(passwd); + if (passwd_hash.empty()) { + result->setErrno(errcode::INTERNAL_ERROR); + break; + } + info->setPasswd(passwd_hash); } if (!bio.empty()) { info->setBio(bio); diff --git a/src/struct.cc b/src/struct.cc index c7103d1..0dbae3e 100644 --- a/src/struct.cc +++ b/src/struct.cc @@ -284,6 +284,42 @@ bool BlogLoginedServlet::handlePre( return true; } +BlogAdminServlet::BlogAdminServlet(const std::string& name) + : BlogLoginedServlet(name) +{ +} + +bool BlogAdminServlet::handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) +{ + if (!BlogLoginedServlet::handlePre(request, response, session, result)) { + return false; + } + + int64_t uid = getUserId(request); + if (!uid) { + result->setErrno(errcode::NOT_LOGIN); + return false; + } + auto user = UserMgr::GetInstance()->get(uid); + if (!user) { + result->setErrno(errcode::USER_NOT_FOUND); + return false; + } + if (user->getState() != UserManager::Status::ACTIVE || user->getIsDeleted()) { + result->setErrno(errcode::ACCOUNT_INVALID); + return false; + } + if (user->getRole() != UserManager::Role::ADMIN) { + result->setErrno(errcode::ACCESS_DENIED); + return false; + } + return true; +} + int64_t BlogServlet::getUserId(chen::http::HttpRequest::ptr request) { std::string sid = request->getCookie(CookieKey::SESSION_KEY); diff --git a/src/struct.h b/src/struct.h index 5c2e43e..5ed90a8 100644 --- a/src/struct.h +++ b/src/struct.h @@ -126,4 +126,20 @@ class BlogLoginedServlet : public BlogServlet { Result::ptr result) override; }; +/** + * @brief 管理员专用 Servlet + * @note 在登录校验基础上要求当前账号状态正常且角色为管理员 + */ +class BlogAdminServlet : public BlogLoginedServlet { +public: + BlogAdminServlet(const std::string& name); + +protected: + bool handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) override; +}; + } // namespace blog diff --git a/src/util.cc b/src/util.cc index 38cfd36..d65a404 100644 --- a/src/util.cc +++ b/src/util.cc @@ -20,4 +20,38 @@ chen::IDB::ptr GetDB() return chen::MySQLMgr::GetInstance()->get(dbs.begin()->first); } -} // namespace blog \ No newline at end of file +std::string HashPassword(const std::string& password) +{ + std::string hash = chen::EncryptorUtil::BCryptHash(password); + if (hash.empty()) { + ERROR(g_logger) << "bcrypt hash password failed"; + } + return hash; +} + +bool VerifyPassword(const std::string& password, const std::string& stored) +{ + if (stored.empty()) { + return false; + } + if (stored.size() > 1 && stored[0] == '$' && stored[1] == '2') { + return chen::EncryptorUtil::BCryptVerify(password, stored); + } + // 兼容历史无盐 MD5,登录成功后由调用方升级为 bcrypt + return stored == chen::EncryptorUtil::MD5(password); +} + +bool IsLegacyPasswordHash(const std::string& stored) +{ + if (stored.size() != 32) { + return false; + } + for (char c : stored) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; +} + +} // namespace blog diff --git a/src/util.h b/src/util.h index fe80cba..bcba2c9 100644 --- a/src/util.h +++ b/src/util.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -19,6 +20,28 @@ namespace blog { chen::IDB::ptr GetDB(); +/** + * @brief 生成密码哈希(bcrypt,含随机盐) + * @param password 明文密码 + * @return bcrypt 哈希;失败返回空串 + */ +std::string HashPassword(const std::string& password); + +/** + * @brief 校验密码,兼容历史无盐 MD5 哈希 + * @param password 明文密码 + * @param stored 数据库中保存的哈希 + * @return 是否匹配 + */ +bool VerifyPassword(const std::string& password, const std::string& stored); + +/** + * @brief 判断是否为历史 MD5 哈希(登录成功后用于透明升级为 bcrypt) + * @param stored 数据库中保存的哈希 + * @return 是否为 32 位十六进制 MD5 + */ +bool IsLegacyPasswordHash(const std::string& stored); + struct DiskUsage { int64_t totalSize; int64_t usedSize;