服务器架设笔记——搭建用户注册和验证功能

        之前介绍的Apache Httpd相关内容,都是些零散的知识点。而实际运用中,我们要根据不同的业务,将这些知识点连接起来以形成各种组合,来满足我们的需求。(转载请指明出于breaksoftware的csdn博客)

        本文我将以用户注册、登陆和免登等这些业务需求,将之前四篇介绍的知识点串起来,形成一组可用的功能。但是,本例子只是为了完成功能,而不涉及相关优化——比如数据库的访问,我觉得是可以优化的——但是优化不是本文的主题。

        网上有很多Apache+PHP的方案,诚然这个组合可以方便快速的搭建业务性功能,但是我不会写PHP,所以我还是用老掉牙的C去写相关模块。

        用户注册和登陆这个大家一般都明白。但是什么叫免登,可能有些同学还不清楚。举个例子,比如我们登陆某网站后,我们再在其子页面中跳转,往往还是处于登陆状态。但是服务器如何确定这个用户的登陆状态,除了像长连接等方案外,通过协议约定也是一种方案。我们约定:在用户成功注册和登陆后,会访问给客户端请求一个加密字段。用户之后的请求都需要带上这个加密字段,以供服务器验证。

接口定义

    注册

      路径:login

      参数:

  • uid        字符串,用户ID
  • pwd      字符串,密码 
  • did        字符串,设备唯一标志
  • action   字符串,行为。注册时该值为new
      返回:
  • res          整型,0 成功 ,1 用户名已存在, 2 其他失败
  • session  字符串,如果res为0, 则该字段有值,否则为空串

    登陆

      路径:login
      参数:
  • uid        字符串,用户ID
  • pwd      字符串,密码 
  • did        字符串,设备唯一标志
      返回:
  • res          整型,0 成功, 其他 失败
  • session  字符串,如果res为0, 则该字段有值,否则为空串

    免登

      路径:任何路径(包括login)

      参数:

  • uid        字符串,用户ID
  • ss          字符串,用于免登的校验标志 
  • did        字符串,设备唯一标志
      返回:根据不同业务,有不同的返回值。免登只是这个请求的一种协助方式。      

模块划分

        根据我们的业务特点,可以拆分出如下4个独立模块(so):
  1. 注册和登陆;
  2. 免登;
  3. 公用库;
  4. json库;这儿需要说明下,我们将使用json作为返回参数的格式。虽然XML是apache apr库中一个可用模块,也是可以用来组织返回数据,但是json还是更加主流。
         服务器架设笔记——搭建用户注册和验证功能_第1张图片

    Json库

        因为apache httpd是C语言写的,所以为了统一风格以及免除之后一切编译相关的问题,我选在了同样的C写的json库——CJson。我们只选用cJSON.h和cJson.c两个文件作为库的原始文件,并编写一个编译脚本

gcc cJSON.c -fPIC -lm -shared -o libcjson.so                                                                                    
cp libcjson.so /usr/local/apache2/modules/

    基础库——utils

        我将基础库分为如下几个功能集:
  1. 加解密;在登陆校验等业务中会使用到。
  2. 编码;base64编码和解码是服务的基础功能。
  3. hash;md5等是必要功能。
  4. 其他辅助函数;一些函数比较复杂,在多个模块中都要被使用到,所以把他们放到基础库中,供各个模块使用。
        编码和Hash没什么好说的,apr库里提供了便捷的方法。加解密在apr-util里也有相应的封装。这儿需要指出的是,我们在编译apr-util时需要指定参数--with-crypto。有的文章上说,还要通过--with-openssl来指定使用openssl库。而我试验发现通过指定该参数,反而会导致加解密模块不可用。因为我们还要使用数据库,所以我们如此编译apr-util
./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr --with-crypto --with-mysql
        我们通过查看/usr/local/apr-util/include/apr-1/apu.h文件中相应宏的定义来知晓相应功能是否已被启用
#define APU_HAVE_PGSQL         0
#define APU_HAVE_MYSQL         1
#define APU_HAVE_SQLITE3       0
#define APU_HAVE_SQLITE2       0
#define APU_HAVE_ORACLE        0
#define APU_HAVE_FREETDS       0
#define APU_HAVE_ODBC          0

#define APU_HAVE_CRYPTO        1                                                                                                                                          
#define APU_HAVE_OPENSSL       1
#define APU_HAVE_NSS           0
        但是非常不幸的是,我参考例子写的一段加解密代码,在测试代码中可以正确运行,而在请求线程中却出现了一些诡异的现象。于是我只能直接使用openssl中的API进行加解密。
        使用如下指令编译openssl,将产出动态链接库,我们将libcrypto.so拷贝到apache httpd的module目录下。
make clean
./config --prefix=/usr/local/openssl
./config shared --prefix=/usr/local/openssl
make depend
make install

     其他模块

        我们其它模块,都会使用到libcjson.so、libutils.so和libcrypto.so。对于这些第三方动态链接文件,我们需要在使用到他们的模块加载之前就加载它们。于是我们配置httpd.conf文件:
LoadFile modules/libcjson.so 
LoadFile modules/libutils.so
LoadFile modules/libcrypto.so

LoadModule user_check_module  modules/mod_user_check.so
LoadModule login_module       modules/mod_login.so
        如此,我们就可以在用户注册登录模块——mod_login.so和免登模块——mod_user_check.so中使用这些API了。

处理流程        

        在之前,我们说到,我们要是的免登逻辑位于所有请求之前。于是我们以注册和登录模块为例子,我们需要在httpd.conf中做如下配置
<Location /login>
    SetHandler user_check
    SetHandler login
</Location>
        user_check模块的代码如下
static int user_check_handler(request_rec *r)
{
    char* uid;
    char* ss;
    char* did;

    user_define_data_ptr = apr_palloc(r->pool, sizeof(user_data));
    user_data* user_data_ptr = (user_data*)r->user_define_data_ptr;
    user_data_ptr->login = 0;
	
    uid = get_args_param(r, "uid");
    ss = get_args_param(r, "ss");
    did = get_args_param(r, "did");
    if (!uid || !ss || !did) {
        return DECLINED;
    }

    user_data_ptr->login = !user_login_ok(r->pool, uid, ss, did);	
    return DECLINED;
}
        注意所有的返回值都是DECLINED。这样内容生成器,才会传导到下一个内容生成器中。
        如果熟悉request_rec结构的同学,可能会马上对上面的代码产生疑问——哪儿来的user_define_data_ptr参数?是的,这个参数不是request_rec默认结构体的成员,是我为了贯通各个内容生成器自行加入的一个变量——修改/usr/local/apache2/include/httpd.h 
/**
 * @brief A structure that represents the current request
 */
struct request_rec {
    /** The pool associated with the request */
    apr_pool_t *pool;
	……
    /** MIME trailer environment from the response */
    apr_table_t *trailers_out; 
    /** define by fangliang*/
    void* user_define_data_ptr;
};
        请求通过免登模块检测后,便已经确认该用户是否已经登录了。然后在其他内容生成其中,通过user_define_data_ptr所指向的结构体对象得知其状态——上下文(如果不想修改源码,可以考虑使用apr_pool_userdata_setn和apr_pool_userdata_get的组合)。
        以上便将所有要点讲解完了,我们可以通过请求相关接口测试相应功能。

特殊问题

        我在链接Mysql数据库时,遇到了Access denied for user ''@'localhost'”的问题。在网上找到一个可行的解决方案,在此做以记录
1.关闭mysql
   # service mysqld stop
2.屏蔽权限
   # mysqld_safe --skip-grant-table
   屏幕出现: Starting demo from .....
3.新开起一个终端输入
   # mysql -u root mysql
   mysql> UPDATE user SET Password=PASSWORD('newpassword') where USER='root';
   mysql> FLUSH PRIVILEGES;//记得要这句话,否则如果关闭先前的终端,又会出现原来的错误
   mysql> \q

代码片段

        以下列出比较有用的代码片段,方便大家使用。

    获取get请求中的参数

char* get_args_param(request_rec* r, const char* name) {
    const char* args = r->args;
    const char* start_args;
    if (!args) {
		return NULL;
	}

	for (start_args = ap_strstr_c(args, name); start_args;
			start_args = ap_strstr_c(start_args + 1, name))
	{
		if (start_args == args || start_args[-1] == '&' || isspace(start_args[-1])) {
			start_args += strlen(name);
			while (*start_args && isspace(*start_args)) {
				++start_args;
			}
			if (*start_args == '=' && start_args[1]) {
				char* end_args;
				char* arg;
				++start_args;
				arg = apr_pstrdup(r->pool, start_args);
				if ((end_args = strchr(arg, '&')) != NULL) {
					*end_args = '\0';
				}
				return arg;
			}
		}
	}
    return NULL;
}

    使用apr库实现md5算法

unsigned char* md5hex(apr_pool_t* pool, const char* in, apr_size_t in_len) {
	unsigned char* out;
	apr_md5_ctx_t context;
	out = apr_palloc(pool, APR_MD5_DIGESTSIZE + 1);
    if (!out) {
		return NULL;
	}

    if (0 != apr_md5_init(&context)) {
		return NULL;
	}

	if (0 != apr_md5_update(&context, in, in_len)) {
		return NULL;
	}

	if (0 != apr_md5_final(out, &context)) {
		return NULL;
	}
	out[APR_MD5_DIGESTSIZE] = '\0';
	return out;
};

char hex2char(int hex) {
	char result = '\0';
	if(hex >= 0 && hex <= 9) {
		result = (char)(hex + 48);
	}
	else if(hex >= 10 && hex <= 15) {
		result = (char)(hex - 10 + 65);
	}
	else {
		result = (char)hex;
	}
	return result;
};

char* md5(apr_pool_t* pool, const char* in, apr_size_t in_len) {
	char* out;
	unsigned char* md5buffer;
	out = apr_palloc(pool, APR_MD5_DIGESTSIZE * 2 + 1);
    if (!out) {
		return NULL;
	}
	
	md5buffer = md5hex(pool, in, in_len);
	if (!md5buffer) {
		return NULL;
	}

	for (apr_size_t index = 0; index < APR_MD5_DIGESTSIZE; index++) {
		unsigned char high;
		unsigned char low;
		unsigned char tmp;
		high = md5buffer[index] >> 4;
		tmp = md5buffer[index] << 4;
		low = tmp >> 4;
		out[2 * index] = hex2char(high);
		out[2 * index + 1] = hex2char(low);
	}
	out[APR_MD5_DIGESTSIZE * 2] = '\0';
	return out;
};

    aes128加解密算法

#include "openssl/evp.h"

apr_size_t aes_128_encrypt(
		apr_pool_t* pool,
		const unsigned char* in,
		apr_size_t in_len,
		const unsigned char* key,
		const unsigned char* iv,
		unsigned char** out)
{
	EVP_CIPHER_CTX	*ctx;
	int len;
	apr_size_t	out_len = 0;

	if (!(ctx = EVP_CIPHER_CTX_new())) {
		return 0;
	}

	if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv)) {
		return 0;
	}
	
	len = (in_len / 16 + 1) *16;
	*out = apr_palloc(pool, len);
	if(1 != EVP_EncryptUpdate(ctx, *out, &len, in, in_len)) {
		return 0;
	}
	out_len = len;
	
	if(1 != EVP_EncryptFinal_ex(ctx, *out + len, &len)) {
		return 0;
	}
	out_len += len;

	EVP_CIPHER_CTX_free(ctx);

	return out_len;
};

apr_size_t aes_128_decrypt(
		apr_pool_t* pool,
		const unsigned char* in,
		apr_size_t in_len,
		const unsigned char* key,
		const unsigned char* iv,
		unsigned char** out)
{
	EVP_CIPHER_CTX	*ctx;
	int len;
	apr_size_t	out_len = 0;

	if (!(ctx = EVP_CIPHER_CTX_new())) {
		return 0;
	}

	if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv)) {
		return 0;
	}

	len = (in_len / 16 + 1) *16;
	*out = apr_palloc(pool, len);
	if(1 != EVP_DecryptUpdate(ctx, *out, &len, in, in_len)) {
		return 0;
	}
	out_len = len;

	if(1 != EVP_DecryptFinal_ex(ctx, *out + len, &len)) {
		return 0;
	}
	out_len += len;

	EVP_CIPHER_CTX_free(ctx);

	return out_len;
};

    更新数据库中的项

apr_status_t updata_db(apr_pool_t* pool, const char* table_name,
		const char* uid, const char* col_name, const char* value)
{
	const apr_dbd_driver_t* driver = NULL;
	apr_dbd_t* handle = NULL;
	apr_dbd_results_t* res = NULL;
	char* sql_cmd;
	apr_status_t status;
	int nrows;
	if (!pool || !uid || !table_name || !col_name || !value) {
		return 1;
	}

	status = apr_dbd_get_driver(pool, "mysql", &driver);
	if (APR_SUCCESS != status) {
		return 1;
	}

	status = apr_dbd_open(driver, pool, "host=localhost;user=root;pass=password;dbname=database_name", &handle);
	if (APR_SUCCESS != status) {
		return 1;
	}

	sql_cmd = apr_psprintf(pool, "update %s set %s=%s where userid='%s'", table_name, col_name, value, uid);
	status = apr_dbd_query(driver, handle, &nrows, sql_cmd);
	if (APR_SUCCESS != status && 1 != nrows) {
		status = 1;
	}
	else {
		status = 0;
	}
	apr_dbd_close(driver, handle);

	return status;
}

    获取数据库中某项

char* get_value(apr_pool_t* pool, const char* uid, const char* col_name) {
	const apr_dbd_driver_t* driver = NULL;
	apr_dbd_t* handle = NULL;
	apr_dbd_results_t* res = NULL;
	char* sql_cmd;
	apr_dbd_row_t* row;
	apr_status_t status;
	char* value = NULL;
	const char* value_tmp = NULL;
	if (!pool || !uid) {
		return NULL;
	}

	status = apr_dbd_get_driver(pool, "mysql", &driver);
	if (APR_SUCCESS != status) {
		return NULL;
	}

	status = apr_dbd_open(driver, pool, "host=localhost;user=root;pass=password;dbname=database_name", &handle);
	if (APR_SUCCESS != status) {
		return NULL;
	}

	sql_cmd = apr_psprintf(pool, "select %s from userlogin where userid='%s'", col_name, uid);
	status = apr_dbd_select(driver, pool, handle, &res, sql_cmd, 0);
	if (APR_SUCCESS != status) {
		value = NULL;
	}
	
	if (0 == apr_dbd_get_row(driver, pool, res, &row, 1)) {
		value_tmp = apr_dbd_get_entry(driver, row, 0);
		value = apr_palloc(pool, 128);
		strcpy(value, value_tmp);
	}
	else {
		value = NULL;
	}
	apr_dbd_close(driver, handle);

	return value;
};
        最后附上模块的代码地址链接: http://pan.baidu.com/s/1dDmAmvZ 密码: c28d

你可能感兴趣的:(服务器架设笔记——搭建用户注册和验证功能)