之前介绍的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):
- 注册和登陆;
- 免登;
- 公用库;
- json库;这儿需要说明下,我们将使用json作为返回参数的格式。虽然XML是apache apr库中一个可用模块,也是可以用来组织返回数据,但是json还是更加主流。
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
我将基础库分为如下几个功能集:
- 加解密;在登陆校验等业务中会使用到。
- 编码;base64编码和解码是服务的基础功能。
- hash;md5等是必要功能。
- 其他辅助函数;一些函数比较复杂,在多个模块中都要被使用到,所以把他们放到基础库中,供各个模块使用。
编码和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