前两篇介绍了:TaskVision自动更新与本地化。这两个功能比较独立,通常我们可以在软件开发完毕后加上,所以从该篇开始,小菜会将这两部份功能去掉,直到最后才会加上。
(一)、引言
TaskVision用户验证采用加密过后的身份验证票(FormsAuthenticationTicket+FormsAuthentication),客户端可通过注册表保存使用DataProtection加密过后的密码。
(二)、效果
正确帐号与密码如下:
帐号:jdoe
密码:welcome
输入帐号与密码:aa,bb
注意了:小菜演示时,密码文本框暂时没有使用PasswordChar属性,为了让大家可以看清输入的密码。
登录提示:
停止IIS站点提示:
输入帐号与密码:jdoe,welcome,选择记住密码,则将帐号与密码保存到注册表,下次就不用输写。
登录成功,在工具栏的右边显示用户全名。
小菜自定义数据验证控件来增强用户体验:(小菜自我感觉良好,代码很清晰,扩展性也不赖。)
你可能会觉得,噢,不过是使用了HelpProvider组件而已,是使用了HelpProvider,但不是而已。(之后会详细介绍自定义数据验证控件)
代码下载:https://files.cnblogs.com/a-peng/SmartClient_Chapter03.rar
(三)、分析
1、数据库部份:
从该篇开始,我们将涉及到TaskVision数据库了,小菜会陆续给出相关表与存储过程。
该篇主要涉及:Users表(用户表)
列名 | 数据类型 | 长度 | 允许空 | 描述 |
UserID | int | 4 | 主键,自动编号 | |
UserName | varchar | 16 | 用户名称 | |
UserPassword | varchar | 16 | 用户密码 | |
UserFullName | varchar | 50 | 用户全名 | |
UserEmail | varchar | 50 | 用户邮箱 | |
IsAccountLocked | bit | 1 | 帐号是否锁定,默认值(0) | |
IsAdministrator | bit | 1 | 用户是否具有管理权限,默认值(0) | |
DateCreated | datetime | 8 | 用户创建时间,默认值(getdate()) |
数据库中已有内容:
(
@UserName varchar(16),
@Password varchar(16)
)
AS
SELECT UserID
FROM Users
WHERE (UserName = @UserName AND cast(UserPassword as varbinary) = cast(@Password as varbinary) AND IsAccountLocked = 0)
使用cast(UserPassword as varbinary)表示将密码使用二进制进行比较,可以区分大小写,如welcome != Welcome
GetUserInfo取用户信息
(
@UserID int
)
AS
SELECT UserID, UserName, UserFullName, UserEmail, IsAdministrator, IsAccountLocked
FROM Users
WHERE (UserID = @UserID)
2、Web服务部份
我们创建了Web站点TaskVisionWS,添加了用于用户验证的Web服务:AuthService.asmx。
数据库相关操作使用:SqlHelper。
该SqlHelper来自Enterprise Library2.0(企业库2.0)中的Microsoft Data Access Application Block(数据访问应用程序块)
企业库中的SqlHelper比PetShop中的SqlHelper功能更强些,连参数都可以不会写。很好很强大。
大家有时间可以看看其源码,不会太难。
题外话:小菜经常逛TerryLee(李会军)的博客,TerryLee写过一个系列:Enterprise Library系列文章回顾与总结
之前一直被Enterprise Library的名字吓到,不敢看,呵,希望大家不要和我一样,勇敢点,你会发现没什么的。
Enterprise Library2.0为微软开源的.Net企业库,包含有:数据访问程序块,日志应用程序块,异常应用程序块,缓存应用程序块,安全应用程序块,加密应用程序块。
可以从这里下载到:http://msdn.microsoft.com/en-us/library/aa480453.aspx 而且都带有例子QuickStartSamples
AuthService.asmx源码:
{
private const string m_connectionString = "data source=localhost;initial catalog=TaskVision;integrated security=SSPI;persist security info=False";
private SqlConnection m_conn;
public AuthService()
{
m_conn = new SqlConnection(m_connectionString);
}
[WebMethod]
// return null:未通过验证,反之通过
public string GetAuthorizationTicket(string userName, string userPassword)
{
string userID = null;
try
{
userID = SqlHelper.ExecuteScalar(m_conn, "AuthenticateUser", userName, userPassword).ToString();
}
catch
{
}
finally
{
if (m_conn.State != ConnectionState.Closed)
m_conn.Close();
}
if (userID == null)
return null;
// 创建身份验证票证有效时间1分钟并加密
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(userID, false, 1);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
// 缓存身份验证票证2分钟
Context.Cache.Insert(encryptedTicket, userID, null, DateTime.Now.AddMinutes(2), TimeSpan.Zero);
return encryptedTicket;
}
[WebMethod()]
// return null:未通过验证,反之通过
public UserInformation GetUserInfo(string ticket)
{
if (!IsTicketValid(ticket))
return null;
// 解密身份验证票证,取出userID
int userID = int.Parse(FormsAuthentication.Decrypt(ticket).Name);
DataTable tbl = null;
try
{
tbl = SqlHelper.ExecuteDataset(m_conn, "GetUserInfo", userID).Tables[0];
}
catch
{ }
finally
{
if (m_conn.State != ConnectionState.Closed)
m_conn.Close();
}
if (tbl == null || tbl.Rows.Count == 0)
return null;
UserInformation userInfo = new UserInformation();
DataRow dr = tbl.Rows[0];
userInfo.UserID = userID;
userInfo.UserName = (string)dr["UserName"];
userInfo.UserFullName = (string)dr["UserFullName"];
userInfo.UserEmail = (string)dr["UserEmail"];
userInfo.IsAdministrator = (bool)dr["IsAdministrator"];
userInfo.IsAccountLocked = (bool)dr["IsAccountLocked"];
return userInfo;
}
// return false:未通过验证的票证,反之通过
private bool IsTicketValid(string ticket)
{
if (ticket == null || Context.Cache[ticket] == null)
{
return false;
}
else
{
int userID = int.Parse(FormsAuthentication.Decrypt(ticket).Name);
DataTable tbl = null;
try
{
tbl = SqlHelper.ExecuteDataset(m_conn, "GetUserInfo", userID).Tables[0];
}
catch
{
}
finally
{
if (m_conn.State != ConnectionState.Closed)
m_conn.Close();
}
if (tbl == null || tbl.Rows.Count == 0)
return false;
UserInformation userInfo = new UserInformation();
DataRow dr = tbl.Rows[0];
userInfo.IsAccountLocked = (bool)dr["IsAccountLocked"];
if (userInfo.IsAccountLocked) // 用户被锁定
return false;
else
return true;
}
}
}
从userID = SqlHelper.ExecuteScalar(m_conn, "AuthenticateUser", userName, userPassword).ToString(); 我们可以看到使用企业库中的SqlHelper执行存储过程,可以不用写参数。感兴趣的朋友可以看看其源码噢。
3、本地应用程序部份
本地应用程序引用Web服务AuthService.asmx,名称为AuthWS
我们创建DataLayer类来封装对Web服务的调用。
using TaskVision.AuthWS;
namespace TaskVision
{
public class DataLayer
{
private string m_ticket;
private AuthService m_authService;
private UserInformation m_currentUser = new UserInformation();
public DataLayer()
{
m_authService = new AuthService();
//InitAuthServiceProxy(m_authService);
}
public UserInformation CurrentUser
{
get
{
return m_currentUser;
}
}
// 用户登录
public WebServicesExceptionType Login(string userName, string userPassword)
{
m_currentUser.UserName = userName;
m_currentUser.UserPassword = userPassword;
WebServicesExceptionType ticketExceptionType = GetAuthorizationTicket();
if (ticketExceptionType == WebServicesExceptionType.None)
{
UserInformation newUser;
try
{
newUser = m_authService.GetUserInfo(m_ticket);
}
catch (Exception ex)
{
return HandleException(ex);
}
if (newUser == null)
return WebServicesExceptionType.AuthenticationException;
m_currentUser.UserID = newUser.UserID;
m_currentUser.UserName = newUser.UserName;
m_currentUser.UserFullName = newUser.UserFullName;
m_currentUser.UserEmail = newUser.UserEmail;
m_currentUser.IsAdministrator = newUser.IsAdministrator;
m_currentUser.IsAccountLocked = newUser.IsAccountLocked;
return WebServicesExceptionType.None;
}
else
{
return ticketExceptionType;
}
}
// 取得用户验证票证
private WebServicesExceptionType GetAuthorizationTicket()
{
try
{
m_ticket = m_authService.GetAuthorizationTicket(m_currentUser.UserName, m_currentUser.UserPassword);
}
catch (Exception ex)
{
m_ticket = null;
return HandleException(ex);
}
if (m_ticket == null)
{
return WebServicesExceptionType.AuthenticationException;
}
return WebServicesExceptionType.None;
}
// 处理异常
private WebServicesExceptionType HandleException(Exception ex)
{
if (ex is System.Net.WebException)
return WebServicesExceptionType.WebException;
else if (ex is System.Web.Services.Protocols.SoapException)
return WebServicesExceptionType.SoapException;
else
return WebServicesExceptionType.Exception;
}
// 初始化用户验证服务
//private void InitAuthServiceProxy(AuthService service)
//{
// string urlSetting = System.Configuration.ConfigurationManager.AppSettings["AuthServiceUrl"];
// if (urlSetting != null)
// {
// service.Url = urlSetting;
// }
// else
// {
// service.Url = "http://localhost/TaskVisionWS/AuthService.asmx";
// }
//}
}
}
你会发现,小菜注释了一部份代码,为何呢?
Web服务的地址会改变,如果我们将其Url地址固定在程序中,会带来很多不便,所以我们通常会将其放在配置文件中如app.Config,然后读取。所以也就有了上面的InitAuthServiceProxy(AuthService service)来初始化该Web服务。然而在.Net2.0中,当我们引用Web服务,VS2005就会自动将引用地址写入app.Config配置文件中,所以无需我们做这部份的内容了。配置文件内容如下:
<configuration>
<configSections>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="TaskVision.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<applicationSettings>
<TaskVision.Properties.Settings>
<setting name="TaskVision_AuthWS_AuthService" serializeAs="String">
<value>http://localhost/TaskVisionWS/AuthService.asmx</value>
</setting>
</TaskVision.Properties.Settings>
</applicationSettings>
</configuration>
如果你对VS2005自定生成的配置文件代码很不满意的话,那你可以使用如下配置文件,然后把代码的注释部份去掉,小菜已经为你准备好了。 :)
<configuration>
<appSettings>
<add key="AuthService" value="http://localhost/TaskVisionWS/AuthService.asmx"/>
</appSettings>
</configuration>
接下来看看用户登录窗体的代码:
using System.Windows.Forms;
using Microsoft.Win32;
namespace TaskVision
{
public partial class LoginForm : Form
{
private const string m_registryKey = @"Software\Microsoft\TaskVision";
private DataLayer m_dataLayer;
public LoginForm(DataLayer dataLayer)
{
InitializeComponent();
m_dataLayer = dataLayer;
}
private void LoginForm_Load(object sender, EventArgs e)
{
lblVersion.Text = "Version " + Application.ProductVersion;
string userName = string.Empty;
string password = string.Empty;
try
{
RegistryKey regKey = Registry.CurrentUser.OpenSubKey(m_registryKey);
if (regKey != null && regKey.GetValue("UserName") != null && regKey.GetValue("Password") != null)
{
userName = regKey.GetValue("UserName").ToString();
password = DataProtection.UnprotectData(regKey.GetValue("Password").ToString());
regKey.Close();
}
}
catch
{
}
if (userName != string.Empty && password != string.Empty)
{
txtUserName.Text = userName;
txtPassword.Text = password;
cbRemember.Checked = true;
}
txtUserName.Focus();
}
private void btnOk_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.None;
if (fvLoginForm.IsValid)
{
this.Cursor = Cursors.WaitCursor;
WebServicesExceptionType loginExceptionType = m_dataLayer.Login(txtUserName.Text, txtPassword.Text);
base.Cursor = Cursors.Arrow;
if (loginExceptionType == WebServicesExceptionType.None) // 登录成功
{
try
{
RegistryKey regKey = Registry.CurrentUser.CreateSubKey(m_registryKey);
if (cbRemember.Checked)
{
regKey.SetValue("UserName", txtUserName.Text);
regKey.SetValue("Password", DataProtection.ProtectData(txtPassword.Text, "TaskVisionPassword"));
}
else
{
regKey.DeleteValue("UserName", false);
regKey.DeleteValue("Password", false);
}
regKey.Close();
}
catch
{
}
this.DialogResult = DialogResult.OK;
this.Close();
}
else
{
txtUserName.Focus();
if (loginExceptionType == WebServicesExceptionType.WebException)
MessageBox.Show("连接服务器超时.", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
else if (loginExceptionType == WebServicesExceptionType.SoapException)
MessageBox.Show("服务器出现异常.", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
else if (loginExceptionType == WebServicesExceptionType.AuthenticationException)
MessageBox.Show("帐号或密码错误.", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
else
MessageBox.Show("未知错误.", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
}
}
}
你会看到在btnOk_Click中有这句代码:if (fvLoginForm.IsValid) { } fvLoginForm为自定义数据验证控件,具体的分析小菜下篇会写,感兴趣的可以先查看小菜的源码。这里表示当帐号与密码两个文本框输入不为空时返回true。
主窗体:
{
private bool m_isWorkOnline = true; // 默认在线工作
private DataLayer m_dataLayer;
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
m_dataLayer = new DataLayer();
if (m_isWorkOnline)
{
if (DisplayLoginForm() == DialogResult.Cancel)
{
this.Close();
return;
}
tbUserName.Text = m_dataLayer.CurrentUser.UserFullName;
}
}
private DialogResult DisplayLoginForm()
{
LoginForm loginForm = new LoginForm(m_dataLayer);
DialogResult loginFormResult = loginForm.ShowDialog();
return loginFormResult;
}
}
作者:a-peng
出处:http://a-peng.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出
原文连接,否则保留追究法律责任的权利。
*************************************************************************