尝试用C#和winform实现一次性身份验证原理实验(基于MySQL5.7数据库)
实验目的
1. 熟悉一次性身份验证的工作原理
2. 熟悉挑战应战的过程
3. 用C#实现一次性身份验证
4. 熟悉散列算法MD5,并会运用
5. 熟悉winform界面化设计
实验环境
- 操作系统:windows 10 家庭版
- 开发工具:Visual Studio 2019
- 语言:C#
- 界面实现:winform窗体控制台程序
实验原理
原理如图所示:
核心思想:
- 利用散列函数MD5的单向性
- 应用挑战应战思想
鉴别流程:
3. 客户端发起认证请求
4. 服务端收到请求后,向客户端发送挑战值N和随机数R
5. 客户端接收挑战值N和R,本地计算应战值(口令的N次MD5迭代值)发回服务端
6. 服务端接收应战值,本地再计算一次MD5散列,于数据库中的h迭代值对比,匹配则认证成功,否则则失败。
注意:
7. 服务端本地数据库中存储的信息有:用户名,口令迭代值h,挑战值N,和随机数R,没有用户的口令密码。此外口令迭代值h始终为H^(N+1)(pwd||R) ,始终比用户端发来的应战值多一次MD5运算,并随着N的变化在变化。
8. 服务器收到有效的应战值后要进行以下几个操作:a.将挑战值N自减1,b.将收到的挑战值作为新的迭代值h更新数据库中原有的h值。
成品效果
数据库存表:
(一些特定的按钮和设计只是为了更好的看清实验步骤,实际运用中没有。)
代码实现
服务端代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using MySql.Data.MySqlClient; //引用MySql
using System.Security.Cryptography; //用于MD5加密
namespace 身份验证//server
{
public partial class Server : Form
{
public Server()
{
InitializeComponent();
//关闭对文本框的非法线程操作检查
TextBox.CheckForIllegalCrossThreadCalls = false;
}
Thread threadWatch = null; //负责监听客户端的线程
Socket socketWatch = null; //负责监听客户端的套接字
static Dictionary<string, Socket> clientConnectionItems = new Dictionary<string, Socket> { };
/// <summary>
/// 启动服务
/// </summary>
/// <param name="socketClientPara"></param>
private void BtnStratSer_Click(object sender, EventArgs e)
{
try
{
//定义一个套接字用于监听客户端发来的信息 包含3个参数(IP4寻址协议,流式连接,TCP协议)
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//服务端发送信息 需要1个IP地址和端口号
IPAddress ipaddress = IPAddress.Parse("127.0.0.1"); //手工绑定ip地址
//将IP地址和端口号绑定到网络节点endpoint上
IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse("8888")); //手工绑定端口号
//监听绑定的网络节点
socketWatch.Bind(endpoint);
//将套接字的监听队列长度限制为20
socketWatch.Listen(20);
//创建一个监听线程
threadWatch = new Thread(WatchConnecting);
//将窗体线程设置为与后台同步
threadWatch.IsBackground = true;
//启动线程
threadWatch.Start();
//启动线程后 txtMsg文本框显示相应提示
TextMsg.AppendText("开始监听客户端传来的信息!" + "\r\n");
this.BtnStratSer.Enabled = false;//启动后“启动”按钮失效
}
catch (Exception ex)
{
TextMsg.AppendText("服务端启动服务失败!" + "\r\n");// \r\n才能换行
this.BtnStratSer.Enabled = true;
}
}
//创建一个负责和客户端通信的套接字
Socket socConnection = null;
/// <summary>
/// 监听用户端
/// </summary>
/// <param name="socketClientPara"></param>
private void WatchConnecting()
{
while (true) //持续不断监听客户端发来的请求
{
socConnection = socketWatch.Accept();
TextMsg.AppendText("客户端连接成功! " + "\r\n");
//创建一个通信线程 pts
ParameterizedThreadStart pts = new ParameterizedThreadStart(ServerRecMsg);
Thread thr = new Thread(pts);
thr.IsBackground = true;
//启动线程
thr.Start(socConnection);
}
}
/// <summary>
/// 接收消息函数
/// </summary>
/// <param name="socketClientPara"></param>
private void ServerSendMsg(string sendMsg)
{
try
{
//将输入的字符串转换成 机器可以识别的字节数组
byte[] arrSendMsg = Encoding.UTF8.GetBytes(sendMsg);
//向客户端发送字节数组信息
socConnection.Send(arrSendMsg);
//将发送的字符串信息附加到文本框txtMsg上
TextMsg.AppendText("服务器:" + GetCurrentTime()+ "\r\n" + sendMsg + "\r\n");
}
catch (Exception ex)
{
TextMsg.AppendText("客户端已断开连接,无法发送信息!" + "\r\n");
}
}
//try=catch语句是为了尝试执行程序,程序没有错误则按顺序执行try内语句,程序出错则执行catch内语句,避免程序崩溃中断
/// <summary>
/// 接收消息函数
/// </summary>
/// <param name="socketClientPara"></param>
private void ServerRecMsg(object socketClientPara)
{
Socket socketServer = socketClientPara as Socket;
while (true)
{
//创建一个内存缓冲区 其大小为1024*1024字节
byte[] arrServerRecMsg = new byte[1024 * 1024];
try
{
//将接收到的信息存入到内存缓冲区,并返回其字节数组的长度
int length = socketServer.Receive(arrServerRecMsg);
//将机器接受到的字节数组转换为人可以读懂的字符串
string strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 0, length);
//将发送的字符串信息附加到文本框txtMsg上
string[] RecMsg = strSRecMsg.Split('_'); //分成RecMsg[0]=标记符,RecMsg[1]=用户名和RecMsg[2]=密码
this.TextMsg.AppendText("客户端:" +GetCurrentTime() + "\r\n" + strSRecMsg + "\r\n");
//这里要开始分情况;1 注册--插入语句 2 登录--查询语句 3 查询当前N,R值
//--------------------------------------------------------------------------------------------------
if (int.Parse(RecMsg[0]) == 1)//注册--插入MySQL --存入数据库username和password,pwd不需要加密
{
//首先检查用户是否已经被注册
string constructorString2 = "server = 127.0.0.1; port = 3306; user = gust; password = gust; database = sk_test;";//root权限太高,出于安全考虑 改用gust
MySqlConnection myConnnect2 = new MySqlConnection(constructorString2);
myConnnect2.Open();//打开
MySqlCommand myCmd2 = new MySqlCommand("SELECT N,R from login2 where username='" + RecMsg[1] + "'; ", myConnnect2);//查找N R sql语句
MySqlDataReader dataReader = myCmd2.ExecuteReader();
if (dataReader.HasRows)
{
MessageBox.Show("该用户已被注册!");
}
else
{
string Enc = Nmd5("1", RecMsg[2]);//MD5加密 可用
string constructorString = "server = 127.0.0.1; port = 3306; user = gust; password = gust; database = sk_test;";//root权限太高,出于安全考虑 改用gust
MySqlConnection myConnnect = new MySqlConnection(constructorString);
myConnnect.Open();//打开
MySqlCommand myCmd = new MySqlCommand($"INSERT INTO login2(username,password,N,R) VALUES('{RecMsg[1]}','{Enc}','{this.TextGetN.Text}','{this.TextGetR.Text}');", myConnnect);//sql语句构造成功!插入完成!!$不可少
if (myCmd.ExecuteNonQuery() > 0)//ExecuteNonQuery()返回影响的行数(用于insert,update,delete),其他关键字返回-1
{
MessageBox.Show("注册成功!插入数据库成功!");//插入数据库成功!
}
this.BtnStratSer.Enabled = true;
}
}
if(int.Parse(RecMsg[0]) == 2)//登录--查询MySQL
{
string Enc = Nmd5("1",RecMsg[2]);//新的算法
//本地再计算一次N次迭代
string constructorString = "server = 127.0.0.1; port = 3306; user = gust; password = gust; database = sk_test;";//root权限太高,出于安全考虑 改用gust
MySqlConnection myConnnect = new MySqlConnection(constructorString);
myConnnect.Open();//打开
MySqlCommand myCmd = new MySqlCommand("UPDATE login2 SET times = times+1,N=N-1,password='" + RecMsg[2] + "' where username = '" + RecMsg[1] + "' and password = '" + Enc + "';", myConnnect);
//更新pwd,N值自减1 times自增1(记录用户登录次数)
if (myCmd.ExecuteNonQuery() > 0)//通过影响的行数!=0 判断语句被执行
{
MessageBox.Show("登录成功!插入数据库成功!");//插入数据库成功!
}
else
{
MessageBox.Show("登录失败!用户名或密码错误!");
}
}
if (int.Parse(RecMsg[0]) == 3)
{ //N=1提示重新注册
string constructorString = "server = 127.0.0.1; port = 3306; user = gust; password = gust; database = sk_test;";//root权限太高,出于安全考虑 改用gust
MySqlConnection myConnnect = new MySqlConnection(constructorString);
myConnnect.Open();//打开
MySqlCommand myCmd = new MySqlCommand("SELECT N,R from login2 where username='" + RecMsg[1] + "'; ", myConnnect);//查找N R sql语句
MySqlDataReader dataReader = myCmd.ExecuteReader();
if (dataReader.HasRows)//判断是否存在N,R
{
//存在N,R
dataReader.Read();
TextGetN.Text = dataReader[0].ToString();//查到的N
TextGetR.Text = dataReader[1].ToString();//查到的R
if (int.Parse(TextGetN.Text) == 1)
{
string constructorString2 = "server = 127.0.0.1; port = 3306; user = gust; password = gust; database = sk_test;";//root权限太高,出于安全考虑 改用gust
MySqlConnection myConnnect2 = new MySqlConnection(constructorString2);
myConnnect2.Open();
MySqlCommand myCmdDel = new MySqlCommand("DELETE from login2 where username='" + RecMsg[1] + "'and N ='1'; ", myConnnect2);//语句正确
if (myCmdDel.ExecuteNonQuery() > 0)
{
MessageBox.Show("N的值为“1”,请重新注册用户!");//插入数据库成功!
int N2 = Ran();
int R2 = Ran2();
TextGetN.Text = N2.ToString();
TextGetR.Text = R2.ToString();
string SendNR = N2.ToString() + "_" + R2.ToString();
ServerSendMsg(SendNR.Trim());//产生新的N R值并发送给客户端
}
else
{
MessageBox.Show("删除语句未正常执行!");
}
}
}
else
{
//不存在N,R
MessageBox.Show("该用户不存在!请先注册!");
//发送新的N,R给用户,方便注册
int N2 = Ran();
int R2 = Ran2();
TextGetN.Text = N2.ToString();
TextGetR.Text = R2.ToString();
string SendNR = N2.ToString() + "_" + R2.ToString();
ServerSendMsg(SendNR.Trim());//产生新的N R值并发送给客户端
}
}
}
catch (Exception ex)
{
TextMsg.AppendText("客户端已断开连接!" + "\r\n");
break;
}
}
}
/// <summary>
/// 发送消息到客户端
/// </summary>
/// <param name="socketClientPara"></param>
private void BtnSendMsg_Click(object sender, EventArgs e)
{
int N = Ran(); //N这里设定为1-10,支持N足够大
int R = Ran2();
this.TextGetN.Text = N.ToString();
this.TextGetR.Text = R.ToString();//从这里 可以拿到N R
string SendNR = N.ToString() + "_" + R.ToString();
ServerSendMsg(SendNR.Trim());//发送N R
}
/// <summary>
/// 查找当前的N,R
/// </summary>
/// <param name="socketClientPara"></param>
private void SelectNR_Click(object sender, EventArgs e)
{
string GN = this.TextGetN.Text;
string GR = this.TextGetR.Text;
string SendNR = GN + "_" + GR;
ServerSendMsg(SendNR.Trim());//发送N R
}
//服务器端只需要把收到的H^N(pwd||R)在做一次md5就好
//11.15 新的尝试 H^2(p)==H(H(P))
/// <summary>
/// MD5加密函数
/// </summary>
/// <param name="socketClientPara"></param>
public static string Nmd5(string a, string text)
{
int n = int.Parse(a);
StringBuilder sb = new StringBuilder();
while (n >= 1)
{
for (int k = 0; k < n; k++)
{
MD5 md5 = MD5.Create();//创建MD5实例
byte[] buffer = System.Text.Encoding.Default.GetBytes(text);//转换成byte[]
byte[] Md5buffer = md5.ComputeHash(buffer);//加密
//转换成16进制
for (int i = 0; i < Md5buffer.Length; i++)
{
sb.Append(Md5buffer[i].ToString("X2")); //x-->将10进制转换为16进制。2-->每次都是两位数输出。
}
md5.Clear();//释放MD5计算空间
text = sb.ToString();
sb.Length = 0;//清空sb,以便下次给text赋值
n -= 1;//n自减1
}
}
return text;
}
//调用 int N =Ran();
//调用 int R =Ran();
public int Ran() //随机数不发送1
{
int num = new int();
Random Random = new Random();
num = Random.Next(5, 21);//5-20 随机数
return num;
} //随机数支持int型数值上限
public int Ran2() //随机数不发送1
{
int num = new int();
Random Random = new Random();
num = Random.Next(5, 31);//5-30 随机数
return num;
}
/// <summary>
/// 获取当前系统时间
/// </summary>
/// <param name="socketClientPara"></param>
private DateTime GetCurrentTime()
{
DateTime currentTime = new DateTime();
currentTime = DateTime.Now;
return currentTime;
}
}
}
by 久违 2019.11.12
客户端代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using MySql.Data.MySqlClient; //引用MySql
using System.Security.Cryptography; //用于MD5加密
namespace 身份验证client
{
public partial class Client : Form
{
public Client()
{
InitializeComponent();
//关闭对文本框的非法线程操作检查
TextBox.CheckForIllegalCrossThreadCalls = false;
}
//创建 1个客户端套接字 和1个负责监听服务端请求的线程
Socket socketClient = null;
Thread threadClient = null;
/// 连接服务端事件
private void BtnConnectSer_Click(object sender, EventArgs e)
{
//定义一个套字节监听 包含3个参数(IP4寻址协议,流式连接,TCP协议)
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//需要获取文本框中的IP地址
IPAddress ipaddress = IPAddress.Parse("127.0.0.1");
//将获取的ip地址和端口号绑定到网络节点endpoint上
IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse("8888"));
//这里客户端套接字连接到网络节点(服务端)用的方法是Connect 而不是Bind
try
{
socketClient.Connect(endpoint);
MessageBox.Show("客户端连接服务端成功!");
//this.TextMsg.AppendText("客户端连接服务端成功!" + "\r\n");//\r\n换行
this.BtnConnectSer.Enabled = false;
//创建一个线程 用于监听服务端发来的消息
threadClient = new Thread(RecMsg);//后面有函数RecMsg
//将窗体线程设置为与后台同步
threadClient.IsBackground = true;
//启动线程
threadClient.Start();
}
catch (Exception ex)
{
MessageBox.Show("远程服务端断开,连接失败!");
}
}
/// <summary>
/// 接收消息函数
/// </summary>
/// <param name="socketClientPara"></param>
public void RecMsg()
{
while (true) //持续监听服务端发来的消息
{
try
{
//定义一个1M的内存缓冲区 用于临时性存储接收到的信息
byte[] arrRecMsg = new byte[1024 * 1024];
//将客户端套接字接收到的数据存入内存缓冲区, 并获取其长度
int length = socketClient.Receive(arrRecMsg);
//将套接字获取到的字节数组转换为人可以看懂的字符串
string strRecMsg = Encoding.UTF8.GetString(arrRecMsg, 0, length);
//string[] RecMsg = strRecMsg.Split('_');
//将发送的信息追加到聊天内容文本框中 GetCurrentTime()获取现在时间的函数
TextMsg.Text= strRecMsg; //只用来接收N R 后面接收的覆盖掉前面的
}
catch (Exception ex)
{
MessageBox.Show("远程服务器已中断连接!");
//this.TextMsg.AppendText("远程服务器已中断连接!" + "\r\n");
this.BtnConnectSer.Enabled = true;
break;
}
}
}
/// <summary>
/// 发送消息函数
/// </summary>
/// <param name="socketClientPara"></param>
private void ClientSendMsg(string sendMsg)
{
try
{
//将输入的内容字符串转换为机器可以识别的字节数组
byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(sendMsg);
//调用客户端套接字发送字节数组
socketClient.Send(arrClientSendMsg);
//将发送的信息追加到聊天内容文本框中
//TextMsg.AppendText("客户端" + GetCurrentTime() + "\r\n" + sendMsg + "\r\n");
}
catch (Exception ex)
{
MessageBox.Show("远程服务器已中断连接,无法发送消息!");
//this.TextMsg.AppendText("远程服务器已中断连接,无法发送消息!" + "\r\n");
}
}
/// <summary>
/// 注册函数
/// </summary>
/// <param name="socketClientPara"></param>
private void BtnZC_Click(object sender, EventArgs e)
{
if (TextName.Text == "")
{
MessageBox.Show("用户名不能为空!");
}
if (TextPwd.Text == "")
{
MessageBox.Show("密码不能为空!");
}
string RecNR = TextMsg.Text;
string[] GetNR = RecNR.Split('_');//GetNR[0]=N GetNR[1]=R
string Enc = Nmd52(GetNR[0], this.TextPwd.Text, GetNR[1]);
string r = "1";//作为标记
string sendTxt = r + '_' + this.TextName.Text + '_' + Enc;//server端把RecMsg[0]作为标识符
ClientSendMsg(sendTxt.Trim()); //整体发送,然后根据_分割成三部分 标记符号_用户名_密码
MessageBox.Show("注册信息提交成功!");
}
/// <summary>
/// 登录函数
/// </summary>
/// <param name="socketClientPara"></param>
//登录的时候接收服务端发来的N和R,然后本地加密之后发送给服务器
private void BtnDL_Click(object sender, EventArgs e)
{
if (TextName.Text == "")
{
MessageBox.Show("用户名不能为空!");
}
if (TextPwd.Text == "")
{
MessageBox.Show("密码不能为空!");
}
if (TextMsg.Text == "")
{
MessageBox.Show("N和R不能为空!");
}
else
{
string RecNR = TextMsg.Text;
string[] GetNR = RecNR.Split('_');//GetNR[0]=N GetNR[1]=R
string r = "2";//作为标记
string Enc = Nmd52(GetNR[0], this.TextPwd.Text, GetNR[1]);
string sendTxt = r + '_' + this.TextName.Text + '_' + Enc/*md5MsgPwd*/;//server端把RecMsg[0]作为标记符
ClientSendMsg(sendTxt.Trim()); //整体发送,然后根据_分割成三部分 标记符_用户名_密码(密码部分后面加密)
MessageBox.Show("用户名和密码提交成功!");
}
}
/// <summary>
/// 查看当前N,R
/// </summary>
/// <param name="socketClientPara"></param>
private void BtnCheck_Click(object sender, EventArgs e)
{
if (TextName.Text == "")
{
MessageBox.Show("用户名不能为空!");
}
else
{
string r = "3";//作为标记
string sendTxt = r + '_' + this.TextName.Text;//根据用户名查找N R
ClientSendMsg(sendTxt.Trim());
MessageBox.Show("检查信息提交成功!");
}
}
/// <summary>
/// MD5加密函数
/// </summary>
/// <param name="socketClientPara"></param>
public static string Nmd5(string a, string text)
{
int n = int.Parse(a);
StringBuilder sb = new StringBuilder();
while (n >= 1)
{
for (int k = 0; k < n; k++)
{
MD5 md5 = MD5.Create();
byte[] buffer = System.Text.Encoding.Default.GetBytes(text);//转换
byte[] Md5buffer = md5.ComputeHash(buffer);//加密
//转换成16进制
for (int i = 0; i < Md5buffer.Length; i++)
{
sb.Append(Md5buffer[i].ToString("X2")); //x-->将10进制转换为16进制。2-->每次都是两位数输出。
}
md5.Clear();
//sb.ToString();
text = sb.ToString();
sb.Length = 0;//清空sb
n -= 1;//n自减1
}
}
return text;
}
//11.15 未调用
public static string Nmd52(string a, string str1, string str2)
{
int n = int.Parse(a);
string text = str1 + str2;
StringBuilder sb = new StringBuilder();
while (n >= 1)
{
for (int k = 0; k < n; k++)
{
MD5 md5 = MD5.Create();
byte[] buffer = System.Text.Encoding.Default.GetBytes(text);//转换
byte[] Md5buffer = md5.ComputeHash(buffer);//加密
//转换成16进制
for (int i = 0; i < Md5buffer.Length; i++)
{
sb.Append(Md5buffer[i].ToString("X2")); //x-->将10进制转换为16进制。2-->每次都是两位数输出。
}
md5.Clear();
//sb.ToString();
text = sb.ToString();
sb.Length = 0;//清空sb
n -= 1;//n自减1
}
}
return text;
}
}
}
by 久违 2019.11.12
总结和思考
1、实验的原理相对简单,但是在代码实现的时候却碰到了很多问题。实验实现的思路大概是服务端和客户端先建立socket连接,服务端和数据库之间连接,然后根据一次性验证的原理在三者之间传值加以验证。
2、首先是socket连接,实现连接的过程不难,但是为了能显示出来服务器和客户端的通信的信息内容,我不得不追加传送的内容到一个显示框,方便观察,也方便两者之间取值。但是不同的传值表示格式不同,而我要实现的功能却很多,包括注册,登录,检查等,所以我最终统一了所有传值的格式,采用“标记符+用户名+迭代值”的方式。标记符用于服务端区分不同的功能选项。例如标记符为1执行注册功能代码,为2执行登录功能代码,为3则执行检查功能代码。同时由于我采用TCP传输,数据一次就发送完成了,因此我不得不将数据在接收的时候分段处理。例如服务端接收客户端发的数据:“1_sun_123”时,根据“_”,将数据分成三部分分别处理。
3、其次是运用C#语言,让服务端和数据库互连得实现。从最开始得导入mysql链接库文件,到连接(打开)数据库,在到编写select,update等语句,看似简单的过程,完整的做下来着实花了我不少时间。这其中有一个难点是,如何才能把数据库的状态反馈给服务端,即如何让服务端知道数据库有没有打开,这条语句有没有执行成功,如何从数据库中寻找值,取值等等。这里我根据不同的sql语句采用了不同的办法,以下是一些用到的函数及其作用:Open()打开数据库,ExecuteNonQuery()返回影响的行数(用于insert,update,delete),其他关键字返回-1,MySqlDataReader dataReader读取数据库内容函数,dataReader.HasRows可以用来判断查询的值在数据库中是否存在,等等
4、最重要的是关于N次迭代的MD5算法的编写。由于C#自带的MD5加密算法只能对字节数组类型的数据进行加密,而传值过后的数据类型是字符串,但是MD5最终的输入又是16进制数,所以我不得不进行多次的数据类型的转换,要将string转成byte[],在转成16进制数。而且这种转换不能一蹴而就,还必须一个一个字符转换,这对我编写加密算法大大提高了难度。长达两个星期我都卡在这个上面。有一个错误困扰了我很久(我单纯的循环了N次byte[]类型的MD5加密过程),就是HN+1(P||R)居然不等于H1(HN(P||R)),这样的话直接让我的程序无法登陆,我都不知道加密出来的东西是啥。这个问题在后来我将整体转换数据类型,加密byte[],转成16进制循环N次之后得到解决,心里一块大石落地。但是即便是这样,最终加密出来的结果严格意义上说还是不对的,但是最起码自己能和自己对上了
5、当N不断的减小成1的时候,我设计的服务端会删除这一天用户字段,同时提醒用户重新注册,这样一来N的存在其实有点像秘钥有效期,N=1时,有效期失效
6、注意一点,本程序是为了验证一次性验证而存在,所以有一定的操作过程,按照操作过程可以实现实时监控传值内容。但是关于“检查”这个按钮我也很纠结,它的设计本意是为了让客户端提醒服务器,服务器从数据库中找到该用户当前的N,R值并且返回给客户端的,让我们能看到N,R的传递过程而存在的,这样用也没有什么问题,但是这也违背的一次性密码验证的性质,一次性认证传递的是动态的哈希过的密码,因为N,R的未知和动态,每一次都不一样,这样也保证了安全。但是当一个非法用户知道我的用户名和密码时,他就可以用这个“检查”按钮得知数据库中当前的N,R只是多少,从而无视动态加密认证过程直接可以登录。我等于是自己给自己挖了一个漏洞,我也是很无奈。后来我也想通了,“检查”这个按钮单纯是为了实验,为了更好的看到实验过程而存在,在生活中是万万不能出现的,毕竟哪有人会为了测试保险箱的保密性把密码告诉别人的。
这次实验遇到的难题不少,对我来说也是一种进步吧,希望自己再接再厉,多往上走一走。
2019.11.15
一些无关实验的感想:
我自己在自学C#和winform窗体控制台程序编写的时候,也是在CSDN上借鉴了很多前辈的文章,终于是修修改改完成了,很感谢在CSDN上发帖的大佬们,看着那么多前辈在种树,我也想加入进来,虽然我的能力还很微弱,不求能帮助到谁, 但是写下来,就算是记录,看看自己的成长的过程,也看看自己的极限在哪里,我觉得我还可以再往上一点,再往上一点。
(11.15就写好了,过了快一个月终于有时间整理出来了,又填上一个自己挖的坑,满足)
2019.12.7