PHP实现“挑战-应答”方式的登陆模型
内容提要
1. 什么是“挑战-应答”系统
2. 关于HMAC
3. 登陆模型的流程与相关代码
“挑战-应答”
定义 假设用户U想向系统S认证自己。设U和S有一个协商好的秘密函数f。挑战-应答认证系统就是这样的一个系统:S发送一个随机消息m(挑战)给U,用户U回应m的变形r=f(m)(应答)。S通过独立计算r来验证r。
根据上面的定义,在一个典型的C/S系统中,认证过程为:
1) 客户向认证服务器发出请求,要求进行身份认证;
2) 认证服务器从用户数据库中查询用户是否是合法的用户,若不是,则不做进一步处理;
3) 认证服务器内部产生一个随机数,作为“提问”,发送给客户;
4) 客户将用户名字和随机数合并,使用单向Hash函数(例如MD5算法)生成一个字节串作为应答;
5) 认证服务器将应答串与自己的计算结果比较,若二者相同,则通过一次认证;否则,认证失败;
6) 认证服务器通知客户认证成功或失败。
在一个C/S项目中,1~3步其实就是客户端请求登陆页面的一个过程。到了第四步,客户将用户名字和随机数合并,生成应答,这个过程使用服务器计算的方式还是在客户端就计算好?使用的函数f(m)又是怎样选择呢?在这里我的办法是,引入HMAC。
HMAC
定义 计算HMAC需要一个散列函数hash(可以是md5或者sha-1)和一个密钥key。用L表示hash函数输出字符串长(md5是16),用B表示数据块的长度(md5和sha-1的分割数据块长都是64)。密钥key的长度可以小于等于数据块长B,如果大于数据块长度,可以使用hash函数对key进行转换,结果就是一个L长的key。
然后创建两个B长的不同字符串:
innerpad = 长度为B的 0×36
outterpad = 长度为B的 0×5C
计算输入字符串str的HMAC:
hash(key ^ outterpad, hash(key ^ innerpad, str))
HMAC主要应用在身份验证中,它的使用方法是这样的:
1. 客户端发出登录请求(假设是浏览器的GET请求)
2. 服务器返回一个随机值,并在会话中记录这个随机值
3. 客户端将该随机值作为密钥,用户密码进行HMAC运算,然后提交给服务器
4. 服务器读取用户数据库中的用户密码和步骤2中发送的随机值做与客户端一样的HMAC运算,然后与用户发送的结果比较,如果结果一致则验证用户合法。
在这个过程中,可能遭到安全攻击的是服务器发送的随机值和用户发送的HMAC结果,而对于截获了这两个值的黑客而言这两个值是没有意义的,绝无获取用户密码的可能性,随机值的引入使HMAC只在当前会话中有效,大大增强了安全性和实用性。大多数的语言都实现了HMAC算法,比如php的mhash、python的hmac.py、java的MessageDigest类,在web验证中使用HMAC也是可行的,用js进行md5运算的速度也是比较快的。事实上,客户端中使用js来计算HMAC,返回结果,在服务器中调用PHP的mhash计算后与客户端返回的结果进行比较。这样做的好处是,服务器避免了重复计算,而且由于客户端在返回结果的过程中,所提交的数据由于经过了HAMC的加密,安全性得到了进一步的提高。
根据上面的分析,整个登陆的模型的设计已经十分明了。由于项目采用的是PHP做为开发语言,同时采用Codeigniter作为框架。相关代码如下:
1. 客户向认证服务器发出请求,要求进行身份认证。这个过程由Login Controller控制,返回用户登陆的页面。其中客户端用来计算HMAC的js可以在这里下载:
http://pajhome.org.uk/crypt/md5/md4.js
LoginView.php
- <?php $attributes = array('onSubmit'=>'javascript:return hex_hmac_md5(key.value, hex_md5(password.value))',
- 'name' => 'loginForm', 'id' => 'loginForm'); ?>
- <?=form_open('login/check_user', $attributes);?>
- //HTML代码,略
- <tr>
- <td width="30%" align="right" height="25">用户名:</td>
- <td align="left"><input name="username" value="" type="text" style="width:130px;" /></td></tr>
- <tr><td align="right" height="25">密 码:<input type="hidden" name="key" value=<?=$key; ?> /></td>
- <td align="left"><input name="password" type="password" value="" style="width:130px;" /></td></tr>
实现的效果如下:
<script src="http://pajhome.org.uk/crypt/md5/md4.js" type=text/javascript></script>
2. 服务器端校验同样由Login Controller控制,调用Model,查询数据库,同时进行校验,并将结果返回Login Controller。
LoginModel.php
- <?php
- class user_model extends Model {
- function __construct() {
- parent::Model();
- //$this->load->library('Hex_hmac');
- }
- function authenticate($username,$password,$key){
- $query = $this->db->get_where('user',array('username'=>$username));
- $admin = $query->row();
- if($admin){
- $data=$admin->password;
- $result=bin2hex (mhash(MHASH_MD5, $data, $key));
- // 若没有启用php_mhash.dll(libmhash) 扩展则使用下面的方式
- //$result=$this->hex_hmac->hmac($key,$data);
- if($result == $password){
- return TRUE;
- }else{
- return FALSE;
- }
- }else{
- return FALSE;
- }
- }
- // 一般的登陆模型:
- function validate_user($username,$password){
- // 用select 1 比较好
- $sql="SELECT 1 FROM user WHERE username = ? AND password = md5(?)";
- $query=$this->db->query($sql,array('username'=>$username,'password'=>$password));
- if ($query->num_rows() > 0) {
- return TRUE;
- } else {
- return FALSE;
- }
- }
- }
- ?>
生成SALT的函数如下:
- function createKey($length=6) {
- $hash = '';
- $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
- $max = strlen ( $chars ) - 1;
- mt_srand ( ( double ) microtime () * 1000000 );
- for($i = 0; $i < $length; $i ++) {
- $hash .= $chars [mt_rand ( 0, $max )];
- }
- return $hash;
- }
问题与分析
1. JavaScript被禁用的情形。很明显,这种情况发生的几率很大——无论是用户的浏览器不支持还是浏览器的设置。在这种情况下,解决的方法只有两个:一是屏蔽这类用户的登陆;二是采用没加密的方式登陆。很明显,采用后者是一个明智的做法,因为这只需对现有的模型稍作修改即可。例如,在用户输入的表单中,增加一个“Hidden field”,在服务器端校验用户返回的结果前,先检查这个“Hidden field”是否经过Hash计算,如否则采用服务器进行两次计算的方式进行校验的过程。
2. 空值问题。这几乎不算一个问题,如果在一般的登陆模型中的话,因为无论是采用客户端js校验或者服务器端校验都能很好解决这个问题。而在这个登陆模型中,事情有一点不一样:经过HMAC计算,即使是空值,也能返回结果!当然,在只需在客户端中增加一段校验的js即可。
3. SALT的产生。SALT由于在客户端和服务器端中要保持同步,产生SALT的函数在登录的过程中务必确保只调用了一次。这里的实现方法是,在登陆中增加一个SALT的“Hidden field”,提交结果的同时,也提交SALT供服务器计算。
4. Password字段的加密。很明显,采用了“挑战-应答”的登陆方式,由于在返回数据之前就已经进行加密,安全性得到很大的提高。而在一般的网站系统中都采用了md5作为加密的方式。但是目前一些网站提供了关于MD5解密后密码和明密的数据库记录,有些网站的记录有上亿条,可以很快查出一次加密后的明码。针对一些弱口令,据说已经达到70%左右。因此采用了md5重复加密的方式,但是由于md5采用单向加密,相同的明文,密码一定是一样的。为什么不能采用与登陆时采用的HMAC?显然,由于HMAC需要随机的SALT,这增加了解密的复杂度。
3. “挑战”即SALT的生成,这里采用的是用PHP生成随机字符串并传到客户端的办法。HMAC与一般的加密重要的区别在于它具有“瞬时”性,即认证只在当时有效。因此用户每次登陆,都将产生新的SALT,故这里并没用加密SALT。