В целом: плохая идея - использовать PlayerPrefs
для конфиденциальных данных. Они хранятся на устройстве в виде необработанного текста, поэтому вы сможете считывать все пароли или изменять их, например,изменить роль пользователя!
Что вы не понимаете в ошибке?
Очевидно, вы пытаетесь .Add
пару ключ-значение к Dictionary
, когда данный key
уже существует в этом Dictionary
.
В AddUser
нет проверки, чтобы поймать такой случай, поэтому, возможно, вам лучше сделать что-то вроде
public static void AddUser(string username, string password, Roles role)
{
// in general check if a string is empty!
if(username.IsNullOrWhitespace())
{
// TODO: SHOW AN ERROR IN THE GUI THAT USERNAME MAY NOT BE EMPTY
Debug.LogWarning("username may not be empty!", this);
return;
}
if(logins.ContainsKey(username))
{
// TODO: SHOW AN ERROR IN THE GUI THAT THIS USER ALREADY EXISTS
Debug.LogWarning($"A User with name \"{username}\" already exists! Please chose another username.", this);
return;
}
logins.Add(username, password);
roles.Add(role);
}
В общем, в вашем коде есть несколько действительно странных порядков, например
Database.AddUser (username.text, password.text, Database.Roles.Teacher);Database.SaveUsers ();Database.LoadUsers ();
Вы добавляете пользователя username.text
, но в SaveUsers
первое, что вы делаете, вызываете LoadUsers
, который сбрасывает оба словаря и затем загружает уже существующих пользователей. ... так что тот, который вы только что создали, потерян.
Или если вы сделаете FactorySettings
PlayerPrefs.DeleteAll();
LoadUsers();
logins = new Dictionary<string, string>();
Сначала вы удалите все PlayerPRefs
, а затем LoadUsers
. зная, что в любом случае не должно быть никакого результата, кроме сброса logins
и roles
, а затем вы снова сбросите logins
. Это довольно избыточно. Вы можете просто сказать
PlayerPrefs.DeleteAll();
logins.Clear();
roles.Clear();
Или еще раз взглянуть на метод SaveUsers
: в вашем цикле foreach
вы делаете
PlayerPrefs.SetString(login.Key, login.Value);
PlayerPrefs.SetString("usernames", "");
PlayerPrefs.SetString("usernames", PlayerPrefs.GetString("usernames") + "/" + login.Key);
, так что, очевидно, вы все равно сохранитекаждая пара username, password
индивидуально ... так зачем вообще использовать другую вещь?
Теперь перейдем к другой вещи: вы сбрасываете PlayerPrefs.SetString("usernames", "");
в каждой итерации цикла ... таквсегда будет храниться только один username/password
!
Также мне кажется немного странным / неуверенным, что для сопоставления имени пользователя с паролем вы используете словарьно когда дело доходит до этой роли пользователя (что почти одинаково важно), вы просто используете список и получаете доступ к нему по индексу.
Итак, в целом
Я бы предпочел использовать подходящий класскак например
[Serializable]
public class User
{
private const SECRET_SALT = "1234My_Example";
public string Name;
// Never store a naked Password!
// Rather store the hash and everytime you want to check the password
// compare the hashes!
public string PasswordHash;
// As said actually you should also not store this as a simple modifiable value
// Since it almost has the same security impact as a password!
public Roles Role;
public User(string name, string password, Roles role)
{
Name = name;
// Instead of the raw password store a hash
PasswordHash = GetHash(password);
Role = role;
}
// Compare the hash of the given password attempt to the stored one
public bool CheckPassword(string attemptedPassword, string hash)
{
var base64AttemptedHash = GetHash(attemptedPassword);
return base64AttemptedHash.Equals(PasswordHash);
}
private static string GetHash(string password)
{
// Use the secret salt so users can not simply edit the stored file
// and add a users password brute-forcing the known blank hashing methods
var unhashedBytes = Encoding.Unicode.GetBytes(SECRET_SALT + password);
var sha256 = new SHA256Managed();
var hashedBytes = sha256.ComputeHash(unhashedBytes);
return Convert.ToBase64String(hashedBytes);
}
}
И затем, например, это как DataBase.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;
public static class DataBase
{
// Do not make the Dictionary public otherwise it can be modified by any class!
// Rather only provide specific setters and getters
private static Dictionary<string, User> _users = new Dictionary<string, User>();
// You might even want to give this bit a less obvious name ;)
private const string fileName = "DataBaseCredentials.cfg";
private static string filePath;
// This method will be automatically called on app start
[InitializeOnLoadMethod]
private static void Initialize()
{
filePath = Path.Combine(Application.persistentDataPath, fileName);
if (!Directory.Exists(Application.persistentDataPath))
{
Directory.CreateDirectory(Application.persistentDataPath);
}
if (!File.Exists(filePath))
{
File.Create(filePath);
}
LoadUsers();
}
private static void LoadUsers()
{
Debug.Log("DataBase: Loading Users from " + filePath);
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
if (stream.Length == 0) return;
var bf = new BinaryFormatter();
_users = (Dictionary<string, User>)bf.Deserialize(stream);
}
}
private static void SaveUsers()
{
Debug.Log("DataBase: Storing Users to " + filePath);
using (var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
{
var bf = new BinaryFormatter();
bf.Serialize(stream, _users);
}
LoadUsers();
}
public static User GetUserByName(string username)
{
if (string.IsNullOrWhiteSpace(username))
{
Debug.LogWarning("username may not be empty!");
return null;
}
if (!_users.ContainsKey(username))
{
Debug.LogWarning($"A user with name \"{username}\" does not exist!");
return null;
}
return _users[username];
}
public static bool LogIn(string username, string password)
{
var user = GetUserByName(username);
return user == null ? false : user.CheckPassword(password);
}
public static void AddUser(string username, string password, Roles role)
{
// Check the name
if (string.IsNullOrWhiteSpace(username))
{
Debug.LogWarning("username may not be empty!");
return;
}
if (_users.ContainsKey(username))
{
Debug.LogWarning($"A user with name \"{username}\" already exists! Chose another username!");
return;
}
_users.Add(username, new User(username, password, role));
SaveUsers();
}
public static void FactorySettings()
{
Debug.Log("FactorySettings!");
_users.Clear();
SaveUsers();
}
}
[Serializable]
public class User
{
public string Name;
public string PasswordHash;
public Roles Role;
public User(string name, string password, Roles role)
{
Name = name;
// Never store a naked Password!
// Rather store the hash and everytime you want to check the password
// compare the hashes!
PasswordHash = GetHash(password);
// As said actually you should also not store this as a simple modifiable value
// Since it almost has the same security impact as a password!
Role = role;
}
private static string GetHash(string password)
{
var unhashedBytes = Encoding.Unicode.GetBytes(password);
var sha256 = new SHA256Managed();
var hashedBytes = sha256.ComputeHash(unhashedBytes);
return Convert.ToBase64String(hashedBytes);
}
public bool CheckPassword(string attemptedPassword)
{
var base64AttemptedHash = GetHash(attemptedPassword);
return base64AttemptedHash.Equals(PasswordHash);
}
}
public enum Roles
{
Teacher,
Student
}
Я знаю, BinaryFormatter
создает довольно большие файлы, и выМожно также сделать это, например, как JSON, но это был самый простой способ показать, как (де) сериализовать Dictionary
в системный файл.
И только для небольшого демонстрационного класса
using UnityEditor;
using UnityEngine;
public class UsersManager : MonoBehaviour
{
public string username;
public string password;
public Roles role;
public User User;
}
[CustomEditor(typeof(UsersManager))]
public class UsersManagerEditor : Editor
{
private UsersManager manager;
private void OnEnable()
{
manager = (UsersManager)target;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("GetUserByName"))
{
manager.User = DataBase.GetUserByName(manager.username);
}
if (GUILayout.Button("AddUser"))
{
DataBase.AddUser(manager.username, manager.password, manager.role);
}
if (GUILayout.Button("CheckPassword"))
{
manager.User = DataBase.GetUserByName(manager.username);
if (manager.User != null)
{
if (manager.User.CheckPassword(manager.password))
{
Debug.Log("Password CORRECT!");
}
else
{
Debug.LogWarning("PASSWORD WRONG!");
}
}
}
if (GUILayout.Button("FactorySettings"))
{
DataBase.FactorySettings();
}
}
}
![enter image description here](https://i.stack.imgur.com/SOCL2.gif)