四、Web登录
整个Web登录过程由两个页面完成,分别是登录页面login.aspx和验证码生成页面idencode.aspx。在登录页面login.aspx上添加三个TextBox控件、一个Image控件和一个Button控件。它们的名称分别为:txtUserName、txtPassWord、txtCode、imgVerify和BtnSubmit,如图1所示。imgVerify用于显示验证码图片,因此它的src值为idencode.aspx,也即由idencode.aspx来生成具体的验证码图片。
在idencode.aspx页的Page_Load写入如下代码:
Passport.VerifyCode vc = new Passport.VerifyCode();
String codeNum=vc.RndNum(3);
Session["DeanIdenCode"] = codeNum;
byte[] ImageStream = vc.CreateVerifyCodeImage(codeNum);
Response.ClearContent();
Response.ContentType = "image/Gif";
Response.BinaryWrite(ImageStream);
程序首先创建随机的汉字验证码字符串,并把这个验证码字符串保存在Session变量里面。再由CreateVerifyCodeImage(string checkCode)函数生成验证图像数据流。通过Response的BinaryWrite的方法将图像数据流输出给客户端,即页面。完成了网页验证码生成的开发。
在页面Login.aspx上控件BtnSubmit的BtnSubmit_Click事件上校验输入的验证码是否正确,用户名和密码输入是否正确。密码输入后需要经过Encrypt(string stringToEncrypt,string sEncryptionKey)函数加密后再和数据库存储的密码进行比对。主要代码如下:
//检查验证码信息,验证用户名、密码信息
string username = txtUserName.Text;
string password = txtPassWord.Text;
string code = txtCode.Text.ToLower();
if (String.Compare(Session["DeanIdenCode"].ToString(), code, true) != 0)
{
MsgLbl.Text = "验证码输入错误";
return;
}
//采用DES加密密码
password = CryptoTool.DESCryptography.Encrypt(password, ConstConfig.sEncryptionKey);
if (UserInfo.CheckUserPassword(username, password))
{ //登录成功,记录用户登录状态信息
……
}
else
{ MsgLbl.Text = "密码或用户名错误,请重新登录";}
以上系统登录通过验证码和密码加密技术很好地解决了系统登录时的安全问题。需要注意的是没有考虑网络传输时的安全,建议采用SSL传输。
五、登录接口
除了直接输出验证码信息,直接连接数据库验证用户名和密码信息外。在很多系统中,系统的登录部分往往会独立出来,提供一个登录接口,供很多受信任的客户端调用。这些客户端可能是Web网页形式的,也可能是WinForm形式的。而开发语言也有多种,如C#、VB.Net、Delphi(严格地说是Pascal)等。
提供Web Service登录接口是最好的解决方案,通过标准的XML形式来交换数据使接口和平台开发语言无关。在实例中整个登录过程需要提供两个WebMethod函数,也即是Web Service提供给客户端调用者的接口方法,一个是UserPreLogin,获取验证码图像信息;一个是UserLogin,验证登录请求。
1. 登录接口
新建一个Web Service项目,命名为WebServices。新增Web Service项UserLoginService.asmx,添加2个方法。其中UserPreLogin函数生成验证码图片信息和客户端识别码,先通过随机生成办法产生验证码字符信息并保存在Session变量里面,随后生成验证码图片字节流并作为函数结果返回。由Guid生成客户端识别码,通过out参数方式返回给客户端。注意UserPreLogin函数需要通过Session保存验证码字符串strCode信息和客户端识别码ClientKey信息供验证登录请求时使用。这就需要打开WebMethod属性EnableSession=true使其支持WebService中的变量做到跨方法使用,也即Session变量VerifyCode_13590和ClientKey_13590在UserLogin函数还可以使用,并能读取保存的值。客户端用CookieContainer来关联Session。UserPreLogin实现代码如下:
// 生成验证码和识别码
// <param name="ClientName">受信任的客户端标识</param>
// <param name="ClientKey">给客户端产生一个GUID识别码</param>
// <returns>返回验证码图片字节流</returns>
[WebMethod(EnableSession=true)]
public byte[] UserPreLogin(string ClientName, out string ClientKey)
{
……//判断是否是受信任的客户端,非信任客户端直接返回退出
Passport.VerifyCode vCode = new Passport.VerifyCode();
string strCode = vCode.RndNum(3);
byte[] ImageStream = vCode.CreateVerifyCodeImage(strCode);
Session["VerifyCode_13590"] = strCode;
ClientKey = Guid.NewGuid().ToString();
Session["ClientKey_13590"] = ClientKey;
return ImageStream;
}
UserLogin提供登录请求验证,如果登录成功返回用户ID号。程序先判断输入的验证码字符、识别码和Session变量保存的值是否一样,再验证用户名和密码,并记录登录的日志信息。通过out类型参数ErrorInfo、WarningInfo返回程序运行的错误信息和警告信息。实现代码如下:
// 验证登录请求
// <param name="LoginName">登录名</param>
// <param name="Password">密码</param>
// <param name="VerifyCode">验证码</param>
// <param name="ClientKey">识别码</param>
// <param name="ErrorInfo">错误信息</param>
// <param name="WarningInfo">警告信息</param>
// <returns>返回用户ID号,如为0表示错误</returns>
[WebMethod(EnableSession = true)]
public int UserLogin(string LoginName, string Password, string VerifyCode, string ClientKey, out string ErrorInfo, out string WarningInfo)
{
int refalse = 0;
ErrorInfo = ""; WarningInfo = "";
string userID = "0";
string newCode = string.Empty;
……//检查用户名、密码、验证码和识别码是否为空
if (Session["VerifyCode_13590"] == null)
{
ErrorInfo = "Session有问题";
return refalse;
}
else
{
newCode = Session["VerifyCode_13590"].ToString();
}
if (!newCode.ToLower().Equals(VerifyCode.ToLower()))
{
ErrorInfo = "验证码输入错误";
return refalse;
}
if (Session["ClientKey_13590"] == null)
{
ErrorInfo = "Session有问题";
return refalse;
}
else if (!ClientKey.Equals(Session["ClientKey_13590"].ToString()))
{
ErrorInfo = "识别码输入错误";
return refalse;
}
//验证用户名和密码
if (UserInfo.CheckUserPassword(LoginName, Password))
{
DataRow dr = UserInfo.GetUserInfo(LoginName);
string userType = string.Empty;
if (dr != null)
{
userID = dr["fUID"].ToString();
userType = dr["fUserType"].ToString();
}
//添加日志
LogHelper.AddUserLog(LogType.Info, Request.UserHostAddress,LoginName,"系统", "登陆成功!");
Session.Remove("VerifyCode_13590");
Session.Remove("ClientKey_13590");
return int.Parse(userID);
}
else
{
ErrorInfo = "用户名或者密码错误";
return refalse;
}
}
2. 客户端调用
根据提供的WebService登录接口可以供多种程序来调用,如C#、VB、Java等B/S、C/S结构的程序。以C#语言开发客户端调用实例详细了解通过WebService登录接口开发用户登录系统的具体实现过程。
建立一个新的Windows Forms项目,命名为DeanClient。增加2个Form窗体frmLogin.cs和frmMain.cs。frmLogin为系统登录窗口,frmMain为登录成功后进入的主程序界面。在frmLogin窗体界面上添加控件TextBox、PictureBox和Button,如图2所示。增加Web引用,URL为http://localhost:2008/WebServices/UserLoginService.asmx,引用名为LoginService。系统会自动生//成代理类Reference.cs。
向项目里添加类ClientUserLogin.cs,编写调用业务逻辑。先实例化Web服务对象,再实例化CookieContainer类为CookiePassport,该对象为Cookie类的实例提供存储空间,用来关联Session。即在调用ulservice.UserPreLogin后把ulservice对象的CookieContainer赋值给CookiePassport,在调用ulservice.UserLogin前把CookiePassport再赋值回ulservice. CookieContainer。保证服务器端的Web Service中的Session值不会丢失。具体代码如下:
//实例化Web服务对象
private static LoginService.UserLoginService ulservice = new LoginService.UserLoginService();
//受信任的客户端标识
private static string ClientName = "DeanClient 1.0";
// 上下传递用的识别码
public static string ClientKey = string.Empty;
//使用的Cookie 集合,不保证每个Cookie 的名字不发生变化。所以采用这种方式
public static System.Net.CookieContainer CookiePassport = new System.Net.CookieContainer();
// 获得验证码和识别码,返回验证码字节流
public static Image UserPreLogin()
{
ulservice.CookieContainer = new System.Net.CookieContainer();
byte[] ImageStream = ulservice.UserPreLogin(ClientName, out ClientKey);
CookiePassport = ulservice.CookieContainer;
Image newImage;
using (MemoryStream ms = new MemoryStream(ImageStream, 0, ImageStream.Length))
{
ms.Write(ImageStream, 0, ImageStream.Length);
newImage = Image.FromStream(ms, true);
}
return newImage;
}
// 登录请求
// <param name="LoginName">登录用户名</param>
// <param name="Password">密码</param>
// <param name="VerifyCode">验证码</param>
public static int UserLogin(string LoginName, string Password, string VerifyCode)
{
string ErrorInfo, WarningInfo;
ulservice.CookieContainer = CookiePassport;
//密码加密
Password = CryptoTool.DESCryptography.Encrypt(Password, ConstConfig.sEncryptionKey);
int t = ulservice.UserLogin(LoginName, Password, VerifyCode, ClientKey, out ErrorInfo, out WarningInfo);
CookiePassport = ulservice.CookieContainer;
if (t==0)
{
MsgBox.ShowError(ErrorInfo, "登录错误信息");
CookiePassport = null; return 0;
}
else
{
if ((WarningInfo != null) && (WarningInfo.Length > 0))
{
MsgBox.ShowWarning(WarningInfo, "登录警告信息"); }
return t;
}
}
frmLogin窗体直接调用ClientUserLogin类的静态方法来实现系统登录,在窗体的Load事件里加入代码:
picExPwd.Image = ClientUserLogin.UserPreLogin();
txtUserName.Focus();
窗体一启动就载入验证码图片信息。登录按钮的Click事件里面加入代码:
//校验输入数据
if (!ValidCheck())
{ return ; }
string LoginUserName = txtUserName.Text.Trim();
string UserPassword = txtPassword.Text.Trim();
string VerifyCode = txtExPwd.Text.Trim();
int t = ClientUserLogin.UserLogin(LoginUserName, UserPassword, VerifyCode);
if (t == 0)
{ picExPwd.Image = ClientUserLogin.UserPreLogin();
btnLogin.Enabled = true;
return;
}
else
{ userID = t;
this.Close();
}
编译运行程序,如图2所示。B/S结构Web页面方式的调用方法也和此类似。只是验证码图片的显示方式不同,是通过前面第四部分描述的靠Response输出显示。用Web Service接口来实现登录过程,依靠XML的通用性,语言、平台无关性,把登录的业务逻辑判断隔离开来。如加入权限控制模块可以实现单点登录。在客户端把密码加密再进行传输有效地保证了密码的安全。通过加入验证码有效地阻挡了攻击。
图2 WinForm调用登录界面
六、USB Key安全性
通过“用户名+密码”的方式来认证识别用户,以及引入密码加密技术和验证码技术都是通过软件的方式来实现。为了进一步提高登录的安全,通用的做法是引入硬件设备。如智能卡、USB Key等。最常用的是USB Key,因为USB Key直接使用计算机标准接口,模样跟普通的U盘差不多,携带方便。USB Key的安全性主要体现在以下三个方面:
1. 硬件PIN码保护
黑客需要同时取得用户的USB Key硬件以及用户的PIN码,才可以登录系统。即使用户的PIN码被泄漏,只要用户持有的USB Key不被盗取,合法用户的身份就不会被仿冒;如果用户的USB Key遗失,拾到者由于不知道用户PIN码,也无法仿冒合法用户的身份。
2. 安全的存储介质
USB Key的密钥存储于安全的介质之中,外部用户无法直接读取,对密钥文件的读写和修改都必须由USB Key内的程序调用。从USB Key接口的外面,没有任何一条命令能够对密钥区的内容进行读出、修改、更新和删除。
3. 硬件实现加密算法
USB Key内置CPU或智能卡芯片,可以实现数据摘要、数据加解密和签名的各种算法,加解密运算在USB Key内进行,保证了用户密钥不会出现在计算机内存中。
七、结语
本文所有程序在VS.NET 2008下测试通过。其实例具有普遍性,对于常用系统的登录验证技术作了介绍。系统的安全性是一个持续完善的过程,所谓“魔高一尺,道高一丈”,在设计登录系统时需考虑多方面的因素。
|