项目介绍:使用drogon 框架实现简单的登录,注册功能,使用Vue3 ,axios,调用drogon后端接口,实现简单的网页功能调用。在总体实现前后端分离项目。

服务器配置:4核+4gb

语言环境:C++

Ubuntu22.0.4

数据库:

后端目录:

前端:vue3目录:
 

后端:

        登录控制器

创建一个login控制器

drogon_ctl create controller -h login

编写login.h: 从请求头中解析出username 和password封装到RegUser 与LoginUser结构体中,传递给函数中。

#pragma once

#include <drogon/HttpController.h>
#include <stdexcept>

using namespace drogon;

struct RegUser {
	std::string username;
	std::string password;
	
};

// 登录专用结构体
struct LoginUser {
	std::string username;
	std::string password;
};

class Login : public drogon::HttpController<Login>
{
  public:
    METHOD_LIST_BEGIN
    // 注册方法
    ADD_METHOD_TO(Login::registe, "/api/reg", Post);
    // 登录方法
    ADD_METHOD_TO(Login::login, "api/login", Post);
    

    METHOD_LIST_END
    // 注册函数声明
    void registe(const HttpRequestPtr& req,
	  std::function<void(const HttpResponsePtr&)>&& callback,RegUser user)const;
    // 登录函数声明
    void login(const HttpRequestPtr& req,
	  std::function<void(const HttpResponsePtr&)>&& callback, LoginUser user)const;
  
    
};







namespace drogon
{
    // 注册用户转换
    template<>
    inline RegUser fromRequest(const HttpRequest& req)
    {
        auto json = req.getJsonObject();
        if (!json) throw std::runtime_error("Invalid JSON");

        RegUser user;
        // 用户名验证
        if (!json->isMember("username") || (*json)["username"].asString().size() < 8)
            throw std::runtime_error("Username too short (min 8 chars)");
        user.username = (*json)["username"].asString();

        // 密码验证
        if (!json->isMember("password") || (*json)["password"].asString().size() < 6)
            throw std::runtime_error("Password too short (min 6 chars)");
        user.password = (*json)["password"].asString();

        // 角色验证
        // if (!json->isMember("role") || (*json)["role"].asString().size() < 0)
        //     throw std::runtime_error("Role too short (min 2 chars)"); // 修正错误信息
        // user.role = (*json)["role"].asString();

        return user;
    }

    // 登录用户转换
    template<>
    inline LoginUser fromRequest(const HttpRequest& req)
    {
        auto json = req.getJsonObject();
        if (!json) throw std::runtime_error("Invalid JSON");

        LoginUser user;
        // 用户名验证
        if (!json->isMember("username") || (*json)["username"].asString().size() < 4)
            throw std::runtime_error("Username too short (min 8 chars)");
        user.username = (*json)["username"].asString();

        // 密码验证
        if (!json->isMember("password") || (*json)["password"].asString().size() < 6)
            throw std::runtime_error("Password too short (min 6 chars)");
        user.password = (*json)["password"].asString();

        return user;
    }


}

编写login.cc:使用异步模型 数据库异步接口,进行嵌套采用非阻塞IO设计(execSqlAsync)通过回调函数链处理嵌套操作,使用drogon自带的hash3 对用户的密码进行加密(安全性设计)。

#include "Login.h"
#include <drogon/orm/Exception.h>
#include <drogon/utils/Utilities.h>

using namespace drogon;
using namespace drogon::orm;

void Login::registe(const HttpRequestPtr& req,
          std::function<void(const HttpResponsePtr&)>&& callback, RegUser user) const
{
    auto dbClient = app().getDbClient();
    auto callbackPtr = std::make_shared<std::function<void(const HttpResponsePtr&)>>(std::move(callback));
    
    // 外层查询
    dbClient->execSqlAsync(
        "SELECT COUNT(*) FROM user WHERE name = ?",
        [this, user, callbackPtr, dbClient](const Result &result) {
            Json::Value json;
            if (result[0][0].as<size_t>() > 0) {
                json["error"] = "该用户名已存在";
                auto resp = HttpResponse::newHttpJsonResponse(json);
                resp->setStatusCode(k409Conflict);
                (*callbackPtr)(resp);
                return;
            }

            // 内层插入
            std::string hashedPassword = utils::getSha3(user.password);
            dbClient->execSqlAsync(
                "INSERT INTO user (name, password) VALUES(?, ?)",
                [callbackPtr](const Result &) {
                    Json::Value innerJson;
                    innerJson["message"] = "注册成功";
                    auto resp = HttpResponse::newHttpJsonResponse(innerJson);
                    resp->setStatusCode(k201Created);
                    (*callbackPtr)(resp);
                },
                [callbackPtr](const DrogonDbException &e) {
                    Json::Value json;
                    json["error"] = "数据库错误: " + std::string(e.base().what());
                    auto resp = HttpResponse::newHttpJsonResponse(json);
                    resp->setStatusCode(k500InternalServerError);
                    (*callbackPtr)(resp);
                },
                user.username,
                hashedPassword
            );
        },
        [callbackPtr](const DrogonDbException &e) {
            Json::Value json;
            json["error"] = "数据库错误: " + std::string(e.base().what());
            auto resp = HttpResponse::newHttpJsonResponse(json);
            resp->setStatusCode(k500InternalServerError);
            (*callbackPtr)(resp);
        },
        user.username
    );
}

void Login::login(const HttpRequestPtr& req,
          std::function<void(const HttpResponsePtr&)>&& callback, LoginUser user) const
{
    auto dbClient = app().getDbClient();
    auto callbackPtr = std::make_shared<std::function<void(const HttpResponsePtr&)>>(std::move(callback));
    
    dbClient->execSqlAsync(
        "SELECT password FROM user WHERE name = ?",
        [user, callbackPtr](const Result &result) {
            Json::Value json;
            if (result.empty()) {
                json["error"] = "用户名或密码错误";
                auto resp = HttpResponse::newHttpJsonResponse(json);
                resp->setStatusCode(k401Unauthorized);
                (*callbackPtr)(resp);
                return;
            }
            
            std::string storedHash = result[0]["password"].as<std::string>();
            std::string inputHash = utils::getSha3(user.password);
            
            if (inputHash != storedHash) {
                json["error"] = "用户名或密码错误";
                auto resp = HttpResponse::newHttpJsonResponse(json);
                resp->setStatusCode(k401Unauthorized);
                (*callbackPtr)(resp);
                return;
            }
            
            json["message"] = "登录成功";
            auto resp = HttpResponse::newHttpJsonResponse(json);
            (*callbackPtr)(resp);
        },
        [callbackPtr](const DrogonDbException &e) {
            Json::Value json;
            json["error"] = "数据库错误: " + std::string(e.base().what());
            auto resp = HttpResponse::newHttpJsonResponse(json);
            resp->setStatusCode(k500InternalServerError);
            (*callbackPtr)(resp);
        },
        user.username
    );
}

        

      

数据库

数据库sql 文件:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
'87CD084D190E436F147322B90E7384F6A8E0676C99D21EF519EA718E51D45F9C');
INSERT INTO `user` VALUES (10, 'xhm1234568', '87CD084D190E436F147322B90E7384F6A8E0676C99D21EF519EA718E51D45F9C');
INSERT INTO `user` VALUES (11, 'xhm123456810', '87CD084D190E436F147322B90E7384F6A8E0676C99D21EF519EA718E51D45F9C');
INSERT INTO `user` VALUES (12, 'test12345', '87CD084D190E436F147322B90E7384F6A8E0676C99D21EF519EA718E51D45F9C');
INSERT INTO `user` VALUES (13, 'xhm12345678', '3D3E655F38CC749FF7CAD28F41FA5472527F765F341ABCA9CD4268B9E2919A8D');

SET FOREIGN_KEY_CHECKS = 1;


前端

        vue3 项目

Login.vue:

<template>
  <div class="login-container">
    <h1>{{ loginStore.isRegistering ? '用户注册' : '用户登录' }}</h1>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label>用户名:</label>
        <input 
          type="text" 
          v-model="loginStore.username"
          placeholder="请输入用户名"
          required
        >
      </div>
      
      <div class="form-group">
        <label>密码:</label>
        <input 
          type="password" 
          v-model="loginStore.password"
          placeholder="请输入密码"
          required
        >
      </div>
      
      <!-- 注册时才显示确认密码 -->
      <div v-if="loginStore.isRegistering" class="form-group">
        <label>确认密码:</label>
        <input 
          type="password" 
          v-model="loginStore.confirmPassword"
          placeholder="请再次输入密码"
          required
        >
      </div>
      
      <button 
        type="submit" 
        :disabled="loginStore.isLoading"
        class="login-btn"
      >
        {{ 
          loginStore.isLoading 
            ? (loginStore.isRegistering ? '注册中...' : '登录中...') 
            : (loginStore.isRegistering ? '注册' : '登录')
        }}
      </button>
      
      <div class="switch-mode">
        <a href="#" @click.prevent="loginStore.toggleRegister">
          {{ loginStore.isRegistering ? '已有账号?立即登录' : '没有账号?立即注册' }}
        </a>
      </div>
      
      <div v-if="loginStore.error" class="error-message">
        {{ loginStore.error }}
      </div>
    </form>
  </div>
</template>

<script setup>
import { useLoginStore } from '@/stores/info.js'
import { useRouter } from 'vue-router'

const router = useRouter()
const loginStore = useLoginStore()

const handleSubmit = async () => {
  try {
    if (loginStore.isRegistering) {
      await loginStore.register()
      // 注册成功后自动切换到登录表单
      loginStore.toggleRegister()
      // 可以添加注册成功的提示
    } else {
      const response = await loginStore.login()
      console.log('登录成功vue:', response)
      router.push('/login') // 登录成功后跳转到首页
    }
  } catch (error) {
    console.error(loginStore.isRegistering ? '注册失败:' : '登录失败:', error)
  }
}
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 2rem auto;
  padding: 2rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
}

.form-group input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.login-btn {
  width: 100%;
  padding: 0.75rem;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 1rem;
}

.login-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.switch-mode {
  text-align: center;
  margin-bottom: 1rem;
}

.switch-mode a {
  color: #42b983;
  text-decoration: none;
}

.switch-mode a:hover {
  text-decoration: underline;
}

.error-message {
  margin-top: 1rem;
  color: #ff5252;
  font-size: 0.9rem;
  text-align: center;
}
</style>

HomeView.vue:
 

<template>
  <div class="login-success-container">
    <!-- 背景图容器 -->
    <div class="background-overlay"></div>
    
    <!-- 内容区域 -->
    <div class="content-wrapper">
      <div class="success-card">
        <h1 class="welcome-title">🎉 登录成功!</h1>
        <p class="welcome-message">欢迎回来,{{ username }}</p>
        <div class="action-area">
          <button class="logout-btn" @click="handleLogout">
            <span class="btn-text">退出登录</span>
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'LoginSuccess',
  data() {
    return {
      username: '用户名称' // 实际应从登录信息获取
    }
  },
  methods: {
    handleLogout() {
      // 这里添加退出登录逻辑
      this.$router.push('/login')
    }
  }
}
</script>

<style scoped>
.login-success-container {
  position: relative;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
}

/* 背景图样式 */
.background-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: 
    linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
    url('../assets/Login-Succcess.png') no-repeat center center;
  background-size: cover;
  filter: blur(2px);
  z-index: 1;
}

.content-wrapper {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.success-card {
  background: rgba(255, 255, 255, 0.95);
  padding: 2.5rem 4rem;
  border-radius: 15px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  text-align: center;
  backdrop-filter: blur(10px);
}

.welcome-title {
  color: #2d3748;
  font-size: 2.5rem;
  margin-bottom: 1rem;
  background: linear-gradient(45deg, #4f46e5, #06b6d4);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.welcome-message {
  color: #4a5568;
  font-size: 1.2rem;
  margin-bottom: 2rem;
}

.logout-btn {
  background: linear-gradient(45deg, #4f46e5, #06b6d4);
  color: white;
  padding: 0.8rem 2rem;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
}

.logout-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}

.btn-text {
  font-size: 1rem;
  font-weight: 500;
}
</style>


接口文件:


        info.js:

import { ref } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'

export const useLoginStore = defineStore('login', () => {
  const username = ref('')
  const password = ref('')
  const confirmPassword = ref('') // 新增确认密码字段
  const isLoading = ref(false)
  const error = ref(null)
  const isLoggedIn = ref(false)
  const isRegistering = ref(false) // 新增注册状态

  // 登录方法(保持不变)
  const login = async () => {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await axios.post('http://192.168.59.132:8080/api/login', {
        username: username.value,
        password: password.value
      })
      
      isLoggedIn.value = true
      return response.data
      
    } catch (err) {
      error.value = err.response?.data?.message || '登录失败,请重试'
      isLoggedIn.value = false
      throw err
    } finally {
      isLoading.value = false
    }
  }

  // 新增注册方法
  const register = async () => {
    isLoading.value = true
    error.value = null
    
    try {
      // 检查密码一致性
      if (password.value !== confirmPassword.value) {
        throw new Error('两次输入的密码不一致')
      }
      
      const response = await axios.post('http://192.168.59.132:8080/api/reg', {
        username: username.value,
        password: password.value
      })
      
      // 注册成功后自动切换到登录状态
      isRegistering.value = false
      return response.data
      
    } catch (err) {
      error.value = err.response?.data?.message || err.message || '注册失败,请重试'
      throw err
    } finally {
      isLoading.value = false
    }
  }

  // 登出方法(保持不变)
  const logout = () => {
    username.value = ''
    password.value = ''
    confirmPassword.value = ''
    isLoggedIn.value = false
  }

  // 新增切换注册/登录状态方法
  const toggleRegister = () => {
    isRegistering.value = !isRegistering.value
    error.value = null
    confirmPassword.value = ''
  }

  return {
    username,
    password,
    confirmPassword, // 导出确认密码字段
    isLoading,
    error,
    isLoggedIn,
    isRegistering, // 导出注册状态
    login,
    register, // 导出注册方法
    logout,
    toggleRegister // 导出切换方法
  }
})

测试

        配置文件优化:

该项目并没有使用redis 需要对redis 的部分进行删除。并通过对数据的连接数提高并发量,通过"number_of_threads": 0,进行配置 默认使用系统使用cpu。

{
  "listeners": [
    {
      "address": "0.0.0.0",
      "port": 8080,
      "https": false
    }
   
  ],

"db_clients": [
    {
        "name": "default",
        "rdbms": "mysql",
        "host": "192.168.59.132", // 数据库地址
        "port": 3307,
        "dbname": "apidata",
        "user": "root",
        "passwd": "123456",
        "client_encoding": "utf8",
        "connection_number": 120,          
        "timeout": 3000,                  
        "auto_reconnect": true,
        "max_idle_time": 30000 ,
        "test_before_use": true  //链接时先测试
    }
],
    "app": {
        //number_of_threads: The number of IO threads, 1 by default, if the value is set to 0, the number of threads
        //is the number of CPU cores
        "number_of_threads": 0,
        //enable_session: False by default
        "enable_session": false,
        "session_timeout": 0,
        //string value of SameSite attribute of the Set-Cookie HTTP response header
        //valid value is either 'Null' (default), 'Lax', 'Strict' or 'None'
        "session_same_site" : "Null",
        //session_cookie_key: The cookie key of the session, "JSESSIONID" by default
        "session_cookie_key": "JSESSIONID",
        //session_max_age: The max age of the session cookie, -1 by default
        "session_max_age": -1,
        //document_root: Root path of HTTP document, default path is ./
        "document_root": "./",
        //home_page: Set the HTML file of the home page, the default value is "index.html"
        //If there isn't any handler registered to the path "/", the home page file in the "document_root" is send to clients as a response
        //to the request for "/".
        "home_page": "index.html",
        //use_implicit_page: enable implicit pages if true, true by default
        "use_implicit_page": true,
        //implicit_page: Set the file which would the server access in a directory that a user accessed.
        //For example, by default, http://localhost/a-directory resolves to http://localhost/a-directory/index.html.
        "implicit_page": "index.html",
        //static_file_headers: Headers for static files
        /*"static_file_headers": [
            {
                "name": "field-name",
                "value": "field-value"
            }
        ],*/
        //upload_path: The path to save the uploaded file. "uploads" by default. 
        //If the path isn't prefixed with /, ./ or ../,
        //it is relative path of document_root path
        "upload_path": "uploads",
        /* file_types:
         * HTTP download file types,The file types supported by drogon
         * by default are "html", "js", "css", "xml", "xsl", "txt", "svg",
         * "ttf", "otf", "woff2", "woff" , "eot", "png", "jpg", "jpeg",
         * "gif", "bmp", "ico", "icns", etc. */
        "file_types": [
            "gif",
            "png",
            "jpg",
            "js",
            "css",
            "html",
            "ico",
            "swf",
            "xap",
            "apk",
            "cur",
            "xml",
            "webp",
            "svg"
        ],
        // mime: A dictionary that extends the internal MIME type support. Maps extensions into new MIME types
        // note: This option only adds MIME to the sever. `file_types` above have to be set for the server to serve them.
        "mime": {
            // "text/markdown": "md",
            // "text/gemini": ["gmi", "gemini"]
        },
        //locations: An array of locations of static files for GET requests.
        "locations": [
            {
                //uri_prefix: The URI prefix of the location prefixed with "/", the default value is "" that disables the location.
                //"uri_prefix": "/.well-known/acme-challenge/",
                //default_content_type: The default content type of the static files without
                //an extension. empty string by default.
                "default_content_type": "text/plain",
                //alias: The location in file system, if it is prefixed with "/", it 
                //presents an absolute path, otherwise it presents a relative path to 
                //the document_root path. 
                //The default value is "" which means use the document root path as the location base path.
                "alias": "",
                //is_case_sensitive: indicates whether the URI prefix is case sensitive.
                "is_case_sensitive": false,
                //allow_all: true by default. If it is set to false, only static files with a valid extension can be accessed.
                "allow_all": true,
                //is_recursive: true by default. If it is set to false, files in sub directories can't be accessed.
                "is_recursive": true,
                //filters: string array, the filters applied to the location.
                "filters": []
            }
        ],
        //max_connections: maximum number of connections, 100000 by default
        "max_connections": 100000,
        //max_connections_per_ip: maximum number of connections per client, 0 by default which means no limit
        "max_connections_per_ip": 0,
        //Load_dynamic_views: False by default, when set to true, drogon
        //compiles and loads dynamically "CSP View Files" in directories defined
        //by "dynamic_views_path"
        "load_dynamic_views": false,
        //dynamic_views_path: If the path isn't prefixed with /, ./ or ../,
        //it is relative path of document_root path
        "dynamic_views_path": [
            "./views"
        ],
        //dynamic_views_output_path: Default by an empty string which means the output path of source 
        //files is the path where the csp files locate. If the path isn't prefixed with /, it is relative 
        //path of the current working directory.
        "dynamic_views_output_path": "",
        //json_parser_stack_limit: 1000 by default, the maximum number of stack depth when reading a json string by the jsoncpp library.
        "json_parser_stack_limit": 1000,
        //enable_unicode_escaping_in_json: true by default, enable unicode escaping in json.
        "enable_unicode_escaping_in_json": true,
        //float_precision_in_json: set precision of float number in json. 
        "float_precision_in_json": {
            //precision: 0 by default, 0 means use the default precision of the jsoncpp lib. 
            "precision": 0,
            //precision_type: must be "significant" or "decimal", defaults to "significant" that means 
            //setting max number of significant digits in string, "decimal" means setting max number of 
            //digits after "." in string
            "precision_type": "significant"
        },
        //log: Set log output, drogon output logs to stdout by default
        "log": {
            //use_spdlog: Use spdlog library to log
            "use_spdlog": false,
            //log_path: Log file path,empty by default,in which case,logs are output to the stdout
            //"log_path": "./",
            //logfile_base_name: Log file base name,empty by default which means drogon names logfile as
            //drogon.log ...
            "logfile_base_name": "",
            //log_size_limit: 100000000 bytes by default,
            //When the log file size reaches "log_size_limit", the log file is switched.
            "log_size_limit": 100000000,
            //max_files: 0 by default,
            //When the number of old log files exceeds "max_files", the oldest file will be deleted. 0 means never delete.
            "max_files": 0,
            //log_level: "DEBUG" by default,options:"TRACE","DEBUG","INFO","WARN"
            //The TRACE level is only valid when built in DEBUG mode.
            "log_level": "DEBUG",
            //display_local_time: false by default, if true, the log time is displayed in local time
            "display_local_time": false
        },
        //run_as_daemon: False by default
        "run_as_daemon": false,
        //handle_sig_term: True by default
        "handle_sig_term": true,
        //relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting;
        "relaunch_on_error": false,
        //use_sendfile: True by default, if true, the program 
        //uses sendfile() system-call to send static files to clients;
        "use_sendfile": true,
        //use_gzip: True by default, use gzip to compress the response body's content;
        "use_gzip": true,
        //use_brotli: False by default, use brotli to compress the response body's content;
        "use_brotli": false,
        //static_files_cache_time: 5 (seconds) by default, the time in which the static file response is cached,
        //0 means cache forever, the negative value means no cache
        "static_files_cache_time": 5,
        //simple_controllers_map: Used to configure mapping from path to simple controller
        //"simple_controllers_map": [
        //    {
        //        "path": "/path/name",
        //        "controller": "controllerClassName",
        //        "http_methods": [
        //            "get",
        //            "post"
        //        ],
        //        "filters": [
        //            "FilterClassName"
        //        ]
        //    }
        //],
        //idle_connection_timeout: Defaults to 60 seconds, the lifetime 
        //of the connection without read or write
        "idle_connection_timeout": 60,
        //server_header_field: Set the 'Server' header field in each response sent by drogon,
        //empty string by default with which the 'Server' header field is set to "Server: drogon/version string\r\n"
        "server_header_field": "",
        //enable_server_header: Set true to force drogon to add a 'Server' header to each HTTP response. The default 
        //value is true.
        "enable_server_header": true,
        //enable_date_header: Set true to force drogon to add a 'Date' header to each HTTP response. The default 
        //value is true.
        "enable_date_header": true,
        //keepalive_requests: Set the maximum number of requests that can be served through one keep-alive connection. 
        //After the maximum number of requests are made, the connection is closed.
        //The default value of 0 means no limit.
        "keepalive_requests": 0,
        //pipelining_requests: Set the maximum number of unhandled requests that can be cached in pipelining buffer. 
        //After the maximum number of requests are made, the connection is closed.
        //The default value of 0 means no limit.
        "pipelining_requests": 0,
        //gzip_static: If it is set to true, when the client requests a static file, drogon first finds the compressed 
        //file with the extension ".gz" in the same path and send the compressed file to the client.
        //The default value of gzip_static is true.
        "gzip_static": true,
        //br_static: If it is set to true, when the client requests a static file, drogon first finds the compressed 
        //file with the extension ".br" in the same path and send the compressed file to the client.
        //The default value of br_static is true.
        "br_static": true,
        //client_max_body_size: Set the maximum body size of HTTP requests received by drogon. The default value is "1M".
        //One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit.
        "client_max_body_size": "1M",
        //max_memory_body_size: Set the maximum body size in memory of HTTP requests received by drogon. The default value is "64K" bytes.
        //If the body size of a HTTP request exceeds this limit, the body is stored to a temporary file for processing.
        //Setting it to "" means no limit.
        "client_max_memory_body_size": "64K",
        //client_max_websocket_message_size: Set the maximum size of messages sent by WebSocket client. The default value is "128K".
        //One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit.
        "client_max_websocket_message_size": "128K",
        //reuse_port: Defaults to false, users can run multiple processes listening on the same port at the same time.
        "reuse_port": false,
        // enabled_compressed_request: Defaults to false. If true the server will automatically decompress compressed request bodies.
        // Currently only gzip and br are supported. Note: max_memory_body_size and max_body_size applies twice for compressed requests.
        // Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request
        // will be rejected.
        "enabled_compressed_request": false,
        // enable_request_stream: Defaults to false. If true the server will enable stream mode for http requests.
        // See the wiki for more details.
        "enable_request_stream": false,
    },
    //plugins: Define all plugins running in the application
    "plugins": [
        {
            //name: The class name of the plugin
            "name": "drogon::plugin::PromExporter",
            //dependencies: Plugins that the plugin depends on. It can be commented out
            "dependencies": [],
            //config: The configuration of the plugin. This json object is the parameter to initialize the plugin.
            //It can be commented out
            "config": {
                "path": "/metrics"
            }
        },
        {
            "name": "drogon::plugin::AccessLogger",
            "dependencies": [],
            "config": {
                "use_spdlog": false,
                "log_path": "",
                "log_format": "",
                "log_file": "access.log",
                "log_size_limit": 0,
                "use_local_time": true,
                "log_index": 0,
                // "show_microseconds": true,
                // "custom_time_format": "",
                // "use_real_ip": false
            }
        }
    ],
    //custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method. 
    "custom_config": {}
}

接口测试:/api/reg:

接口测试:/api/login
 

并发量测试:

效果展示:

登录成功:

项目总结

技术实现
  • 使用 Drogon C++ 框架实现后端 RESTful API,提供登录和注册功能。
  • 前端采用 Vue3 和 Axios 进行接口调用,实现前后端分离架构。
  • 数据库操作通过异步 SQL 查询实现,密码使用 SHA3 哈希存储。
  • 用户输入验证包括用户名长度、密码复杂度等基本校验。
  • 并通过对数据库和运行负载的cpu进行优化调度,在apipost 得到QFS>2k/s并发量。
功能完整性
  • 实现了核心的注册和登录流程,包括用户名冲突检测。
  • 前后端数据交互使用 JSON 格式,符合现代 Web 开发规范。
  • 错误处理机制涵盖数据库异常、输入验证失败等场景。
代码结构
  • 后端控制器采用 Drogon 的 HttpController 设计模式,路由清晰。
  • 请求参数通过模板特化的 fromRequest 实现自动转换。
  • 嵌套的异步数据库操作保证了数据一致性。

不足之处

安全性

短期优化
  • 缺少 JWT 或 Session 等完整的身份认证机制。
  • 密码哈希未使用盐值(Salt),存在彩虹表攻击风险。
  • 未实现登录失败次数限制等防暴力破解措施。
  • 补充 JWT 鉴权中间件实现
  • 为密码哈希添加随机盐值
  • 统一验证规则和错误提示文案
  • 完善前端页面与后端接口的联调细节
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐