454 lines
18 KiB
C#
454 lines
18 KiB
C#
using Lskj.Control;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.Security.Cryptography;
|
||
using System.Security.Principal;
|
||
using System.Text;
|
||
using System.Windows.Forms;
|
||
|
||
namespace Lskj.GetSecretKey
|
||
{
|
||
public partial class LoginFrm : BaseForm
|
||
{
|
||
private ContextMenuStrip _ctx;
|
||
|
||
public LoginFrm()
|
||
{
|
||
InitializeComponent();
|
||
SecureUserStore.EnsureInitialized(); // 初始化本地存储(含默认账号)
|
||
InitUi();
|
||
WireEvents();
|
||
}
|
||
|
||
#region UI & 事件
|
||
private void InitUi()
|
||
{
|
||
// 密码框设置为 * 掩码(兼容 TextBox / DevExpress TextEdit)
|
||
SetPasswordMask(passWordEidt);
|
||
|
||
// 回填上次成功登录账号
|
||
var last = SecureUserStore.GetLastUser();
|
||
if (!string.IsNullOrEmpty(last)) { userNameEdit.Text = last; passWordEidt.Focus(); }
|
||
else userNameEdit.Focus();
|
||
|
||
// 提示
|
||
try
|
||
{
|
||
setPanel.Cursor = Cursors.Hand;
|
||
var tip = new ToolTip();
|
||
tip.SetToolTip(setPanel, "左键:修改密码(需当前密码验证)\r\n右键:更多操作(重置密码…)");
|
||
}
|
||
catch { /* ignore */ }
|
||
|
||
// 右键菜单
|
||
_ctx = new ContextMenuStrip();
|
||
var miChange = new ToolStripMenuItem("修改密码…", null, (s, e) => DoChangePassword());
|
||
var miReset = new ToolStripMenuItem("重置密码…", null, (s, e) => DoResetPassword());
|
||
_ctx.Items.AddRange(new ToolStripItem[] { miChange, new ToolStripSeparator(), miReset });
|
||
setPanel.ContextMenuStrip = _ctx;
|
||
}
|
||
|
||
private void WireEvents()
|
||
{
|
||
// 回车登录
|
||
passWordEidt.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) TryLogin(); };
|
||
|
||
// 左键点击 setPanel -> 修改密码(需当前密码验证通过)
|
||
setPanel.Click += (s, e) => {
|
||
if (e is MouseEventArgs me && me.Button == MouseButtons.Right) return;
|
||
DoChangePassword();
|
||
};
|
||
|
||
// 右键已经由 ContextMenuStrip 处理
|
||
}
|
||
#endregion
|
||
|
||
#region 登录/改密/重置
|
||
private void TryLogin()
|
||
{
|
||
string user = (userNameEdit.Text ?? "").Trim();
|
||
string pwd = passWordEidt.Text ?? "";
|
||
|
||
if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pwd))
|
||
{
|
||
MessageBox.Show("请输入账号和密码。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
if (SecureUserStore.Validate(user, pwd))
|
||
{
|
||
SecureUserStore.SetLastUser(user); // 记住本次成功登录账号
|
||
this.DialogResult = DialogResult.OK;
|
||
this.Close();
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("账号或密码错误。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
passWordEidt.SelectAll();
|
||
passWordEidt.Focus();
|
||
}
|
||
}
|
||
|
||
private void DoChangePassword()
|
||
{
|
||
string user = (userNameEdit.Text ?? "").Trim();
|
||
string currPwd = passWordEidt.Text ?? "";
|
||
|
||
if (string.IsNullOrEmpty(user))
|
||
{
|
||
MessageBox.Show("请先在账号框输入要修改密码的账号(例如:管理员、管理员1)。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
userNameEdit.Focus();
|
||
return;
|
||
}
|
||
if (!SecureUserStore.Exists(user))
|
||
{
|
||
MessageBox.Show("账号不存在,仅支持:管理员、管理员1。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
userNameEdit.Focus(); userNameEdit.SelectAll();
|
||
return;
|
||
}
|
||
// 必须先通过“账号+当前密码”验证
|
||
if (!SecureUserStore.Validate(user, currPwd))
|
||
{
|
||
MessageBox.Show("请先在密码框输入该账号的当前密码并验证通过,然后再修改。", "需要验证", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
passWordEidt.Focus(); passWordEidt.SelectAll();
|
||
return;
|
||
}
|
||
|
||
using (var dlg = new ChangePasswordDialog(user))
|
||
{
|
||
if (dlg.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
SecureUserStore.SetPassword(user, dlg.NewPassword);
|
||
MessageBox.Show("密码已更新并安全保存到本地。", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DoResetPassword()
|
||
{
|
||
// 仅管理员权限允许重置
|
||
if (!SecurityUtil.IsAdministrator())
|
||
{
|
||
MessageBox.Show("需要以【管理员身份运行】程序后才能重置密码。", "权限不足", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
using (var dlg = new ResetConfirmDialog())
|
||
{
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
// 校验重置口令(你可以修改 SecureReset.ResetSecret 自定义口令)
|
||
if (!SecureReset.VerifyResetCode(dlg.ResetCode))
|
||
{
|
||
MessageBox.Show("重置口令不正确。", "校验失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
}
|
||
|
||
string user = (userNameEdit.Text ?? "").Trim();
|
||
|
||
if (string.IsNullOrEmpty(user))
|
||
{
|
||
// 未指定账号 -> 询问是否重置所有内置账号
|
||
if (MessageBox.Show("未填写账号,是否重置所有内置账号为默认密码 123456?",
|
||
"重置确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
|
||
{
|
||
foreach (var u in SecureUserStore.BuiltinUsers) SecureUserStore.ResetToDefault(u);
|
||
MessageBox.Show("所有内置账号已重置为 123456。", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (!SecureUserStore.Exists(user))
|
||
{
|
||
MessageBox.Show("账号不存在,无法重置。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
if (MessageBox.Show($"确定将「{user}」密码重置为 123456?",
|
||
"重置确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
|
||
{
|
||
SecureUserStore.ResetToDefault(user);
|
||
MessageBox.Show("已重置为 123456。", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 工具:为控件设置 * 掩码(兼容 TextBox / DevExpress TextEdit)
|
||
private static void SetPasswordMask(System.Windows.Forms.Control ctrl)
|
||
{
|
||
try
|
||
{
|
||
var t = ctrl.GetType();
|
||
var propPwdChar = t.GetProperty("PasswordChar");
|
||
if (propPwdChar != null && propPwdChar.CanWrite) propPwdChar.SetValue(ctrl, '*', null);
|
||
|
||
var propUseSys = t.GetProperty("UseSystemPasswordChar");
|
||
if (propUseSys != null && propUseSys.CanWrite) propUseSys.SetValue(ctrl, true, null);
|
||
|
||
var propProperties = t.GetProperty("Properties");
|
||
if (propProperties != null)
|
||
{
|
||
var propsObj = propProperties.GetValue(ctrl, null);
|
||
if (propsObj != null)
|
||
{
|
||
var px = propsObj.GetType();
|
||
var p1 = px.GetProperty("PasswordChar"); if (p1 != null && p1.CanWrite) p1.SetValue(propsObj, '*', null);
|
||
var p2 = px.GetProperty("UseSystemPasswordChar"); if (p2 != null && p2.CanWrite) p2.SetValue(propsObj, true, null);
|
||
}
|
||
}
|
||
}
|
||
catch { /* ignore */ }
|
||
}
|
||
#endregion
|
||
}
|
||
|
||
#region 安全工具 & 重置口令校验
|
||
internal static class SecurityUtil
|
||
{
|
||
public static bool IsAdministrator()
|
||
{
|
||
try
|
||
{
|
||
using (var identity = WindowsIdentity.GetCurrent())
|
||
{
|
||
var principal = new WindowsPrincipal(identity);
|
||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||
}
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
public static string Sha256(string s)
|
||
{
|
||
using (var sha = SHA256.Create())
|
||
{
|
||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(s ?? ""));
|
||
var sb = new StringBuilder(bytes.Length * 2);
|
||
foreach (var b in bytes) sb.Append(b.ToString("x2"));
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|
||
|
||
internal static class SecureReset
|
||
{
|
||
// ★ 重置口令(明文)。建议你改成自己的:仅内部人员知悉。
|
||
private const string ResetSecret = "Lskj#Reset2025";
|
||
private static readonly string ResetHash = SecurityUtil.Sha256(ResetSecret);
|
||
|
||
public static bool VerifyResetCode(string input)
|
||
=> string.Equals(SecurityUtil.Sha256(input ?? ""), ResetHash, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
#endregion
|
||
|
||
#region 本地安全存储(DPAPI 加密 + 记住上次账号)
|
||
internal static class SecureUserStore
|
||
{
|
||
// %LOCALAPPDATA%\Lskj.GetSecretKey\
|
||
private static readonly string AppDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Lskj.GetSecretKey");
|
||
private static readonly string StorePath = Path.Combine(AppDir, "users.sec"); // 用户: 密文
|
||
private static readonly string LastUserPath = Path.Combine(AppDir, "lastuser.dat"); // 纯文本:上一次成功登录账号
|
||
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("Lskj.GetSecretKey|DPAPI|v1");
|
||
internal static readonly string[] BuiltinUsers = new[] { "管理员", "管理员1" }; // 供外部枚举使用
|
||
private const string DefaultPassword = "123456";
|
||
|
||
// 缓存:用户名 -> Base64(Protect(UTF8(pwd)))
|
||
private static readonly Dictionary<string, string> _cipherMap = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
private static bool _initialized;
|
||
private static readonly object _lock = new object();
|
||
|
||
public static void EnsureInitialized()
|
||
{
|
||
if (_initialized) return;
|
||
lock (_lock)
|
||
{
|
||
if (_initialized) return;
|
||
Directory.CreateDirectory(AppDir);
|
||
|
||
if (!File.Exists(StorePath))
|
||
{
|
||
foreach (var u in BuiltinUsers) _cipherMap[u] = ProtectToBase64(DefaultPassword);
|
||
Persist();
|
||
}
|
||
else
|
||
{
|
||
Load();
|
||
bool changed = false;
|
||
foreach (var u in BuiltinUsers)
|
||
if (!_cipherMap.ContainsKey(u)) { _cipherMap[u] = ProtectToBase64(DefaultPassword); changed = true; }
|
||
if (changed) Persist();
|
||
}
|
||
_initialized = true;
|
||
}
|
||
}
|
||
|
||
public static bool Exists(string user) { EnsureInitialized(); return _cipherMap.ContainsKey(user); }
|
||
|
||
public static bool Validate(string user, string plainPwd)
|
||
{
|
||
EnsureInitialized();
|
||
if (!_cipherMap.TryGetValue(user, out var b64)) return false;
|
||
try { return UnprotectFromBase64(b64) == (plainPwd ?? ""); }
|
||
catch { return false; }
|
||
}
|
||
|
||
public static void SetPassword(string user, string newPwd)
|
||
{
|
||
EnsureInitialized();
|
||
if (string.IsNullOrEmpty(user)) return;
|
||
_cipherMap[user] = ProtectToBase64(newPwd ?? "");
|
||
Persist();
|
||
}
|
||
|
||
public static void ResetToDefault(string user)
|
||
{
|
||
EnsureInitialized();
|
||
if (string.IsNullOrEmpty(user)) return;
|
||
_cipherMap[user] = ProtectToBase64(DefaultPassword);
|
||
Persist();
|
||
}
|
||
|
||
public static void SetLastUser(string user)
|
||
{
|
||
try { File.WriteAllText(LastUserPath, user ?? "", Encoding.UTF8); }
|
||
catch { /* ignore */ }
|
||
}
|
||
|
||
public static string GetLastUser()
|
||
{
|
||
try { return File.Exists(LastUserPath) ? (File.ReadAllText(LastUserPath, Encoding.UTF8).Trim()) : ""; }
|
||
catch { return ""; }
|
||
}
|
||
|
||
private static void Load()
|
||
{
|
||
_cipherMap.Clear();
|
||
foreach (var line in File.ReadAllLines(StorePath, Encoding.UTF8))
|
||
{
|
||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||
var idx = line.IndexOf('=');
|
||
if (idx <= 0) continue;
|
||
var key = line.Substring(0, idx);
|
||
var val = line.Substring(idx + 1);
|
||
if (!_cipherMap.ContainsKey(key)) _cipherMap[key] = val;
|
||
}
|
||
}
|
||
|
||
private static void Persist()
|
||
{
|
||
var sb = new StringBuilder();
|
||
foreach (var kv in _cipherMap) sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
|
||
File.WriteAllText(StorePath, sb.ToString(), Encoding.UTF8);
|
||
}
|
||
|
||
private static string ProtectToBase64(string plain)
|
||
{
|
||
var bytes = Encoding.UTF8.GetBytes(plain ?? "");
|
||
var cipher = ProtectedData.Protect(bytes, Entropy, DataProtectionScope.CurrentUser);
|
||
return Convert.ToBase64String(cipher);
|
||
}
|
||
|
||
private static string UnprotectFromBase64(string base64)
|
||
{
|
||
var cipher = Convert.FromBase64String(base64 ?? "");
|
||
var plain = ProtectedData.Unprotect(cipher, Entropy, DataProtectionScope.CurrentUser);
|
||
return Encoding.UTF8.GetString(plain);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 修改密码对话框
|
||
internal sealed class ChangePasswordDialog : Form
|
||
{
|
||
private readonly string _user;
|
||
private TextBox txtNew, txtConfirm;
|
||
private Button btnOk, btnCancel;
|
||
public string NewPassword { get; private set; }
|
||
|
||
public ChangePasswordDialog(string user)
|
||
{
|
||
_user = user;
|
||
BuildUi();
|
||
}
|
||
|
||
private void BuildUi()
|
||
{
|
||
this.Text = $"修改密码 - {_user}";
|
||
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
this.StartPosition = FormStartPosition.CenterParent;
|
||
this.ClientSize = new Size(340, 170);
|
||
this.MaximizeBox = false; this.MinimizeBox = false;
|
||
|
||
var lbl1 = new Label { Text = "新密码:", AutoSize = true, Location = new Point(20, 28) };
|
||
var lbl2 = new Label { Text = "确认新密码:", AutoSize = true, Location = new Point(20, 68) };
|
||
|
||
txtNew = new TextBox { Location = new Point(130, 24), Width = 180, UseSystemPasswordChar = true, PasswordChar = '*' };
|
||
txtConfirm = new TextBox { Location = new Point(130, 64), Width = 180, UseSystemPasswordChar = true, PasswordChar = '*' };
|
||
|
||
btnOk = new Button { Text = "确定", DialogResult = DialogResult.OK, Location = new Point(130, 110), Width = 80 };
|
||
btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Location = new Point(230, 110), Width = 80 };
|
||
|
||
btnOk.Click += (s, e) =>
|
||
{
|
||
if (string.IsNullOrEmpty(txtNew.Text))
|
||
{
|
||
MessageBox.Show("新密码不能为空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
this.DialogResult = DialogResult.None; return;
|
||
}
|
||
if (txtNew.Text != txtConfirm.Text)
|
||
{
|
||
MessageBox.Show("两次输入的密码不一致。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
this.DialogResult = DialogResult.None; return;
|
||
}
|
||
NewPassword = txtNew.Text;
|
||
};
|
||
|
||
this.Controls.AddRange(new System.Windows.Forms.Control[] { lbl1, txtNew, lbl2, txtConfirm, btnOk, btnCancel });
|
||
this.AcceptButton = btnOk; this.CancelButton = btnCancel;
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 重置口令对话框
|
||
internal sealed class ResetConfirmDialog : Form
|
||
{
|
||
private TextBox txtCode;
|
||
private Button btnOk, btnCancel;
|
||
public string ResetCode { get; private set; }
|
||
|
||
public ResetConfirmDialog() { BuildUi(); }
|
||
|
||
private void BuildUi()
|
||
{
|
||
this.Text = "输入重置口令";
|
||
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
this.StartPosition = FormStartPosition.CenterParent;
|
||
this.ClientSize = new Size(360, 150);
|
||
this.MaximizeBox = false; this.MinimizeBox = false;
|
||
|
||
var lbl = new Label { Text = "重置口令:", AutoSize = true, Location = new Point(20, 30) };
|
||
txtCode = new TextBox { Location = new Point(100, 26), Width = 220, UseSystemPasswordChar = true, PasswordChar = '*' };
|
||
|
||
btnOk = new Button { Text = "确定", DialogResult = DialogResult.OK, Location = new Point(160, 80), Width = 75 };
|
||
btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Location = new Point(245, 80), Width = 75 };
|
||
|
||
btnOk.Click += (s, e) =>
|
||
{
|
||
if (string.IsNullOrEmpty(txtCode.Text))
|
||
{
|
||
MessageBox.Show("请输入重置口令。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
this.DialogResult = DialogResult.None; return;
|
||
}
|
||
ResetCode = txtCode.Text;
|
||
};
|
||
|
||
this.Controls.AddRange(new System.Windows.Forms.Control[] { lbl, txtCode, btnOk, btnCancel });
|
||
this.AcceptButton = btnOk; this.CancelButton = btnCancel;
|
||
}
|
||
}
|
||
#endregion
|
||
}
|