自定义 Tokenization 数据库优先





5.00/5 (2投票s)
这种自定义数据库优先的令牌身份验证方法将使您能够以简单的方式在您的应用程序中应用基于令牌的身份验证,而不会有任何麻烦。
引言
当我们希望阻止未经授权的用户从我们的应用程序访问数据时,我们需要在我们的应用程序中应用基于令牌的身份验证,以便只有有效的用户才能访问数据。 通常,ASP.NET Web API 2 提供了基于身份的身份验证,这通常在代码优先的方法中实现。 但是,在某些情况下,我们必须采用数据库优先的方法,在这种情况下,身份令牌的实现可能会遇到一些麻烦。 这种自定义数据库优先的令牌身份验证方法将使您能够以简单的方式在您的应用程序中应用基于令牌的身份验证,而不会有任何麻烦。 当我们希望阻止未经授权的用户从我们的应用程序访问数据时,我们需要在我们的应用程序中应用基于令牌的身份验证,以便只有有效的用户才能访问数据。
实施步骤
您需要按照以下步骤来实现自定义基于令牌的身份验证
步骤 1
创建一个名为 Tokenization
的数据库,并在其中添加 3 个表,如下所示
CREATE DATABASE Tokenization
USE Tokenization
GO
CREATE TABLE Employees
(
EmployeeId INT PRIMARY KEY IDENTITY(1,1) NOT NULL,
EmployeeName VARCHAR(255) NULL,
EmployeeEmail VARCHAR(255) NULL,
EmployeeMobileNumber VARCHAR(12) NULL,
EmployeeAddress VARCHAR(255) NULL
)
GO
CREATE TABLE Users
(
UserId INT PRIMARY KEY IDENTITY(1,1) NOT NULL,
FullName VARCHAR(255) NULL,
LoginName VARCHAR(255) NOT NULL,
PasswordNo VARCHAR(255) NOT NULL,
EmployeeId INT NOT NULL
)
GO
CREATE TABLE TokenManager
(
TokenID BIGINT PRIMARY KEY IDENTITY(1,1) NOT NULL,
TokenKey VARCHAR(255) NULL,
IssuedOn DATETIME NULL,
ExpiresOn DATETIME NULL,
CreatedOn DATETIME NULL,
UserId INT NULL
)
GO
第二步
创建一个 Web API 2 项目,并将其命名为 CustomTokenizationDatabaseFirst
:请参阅下图
步骤 3
通过右键单击 CustomTokenizationDatabaseFirst
项目创建一个文件夹,并将其命名为 TokenFilter
:在此文件夹下,创建一个名为 ApiAuthorizeAttribute
的类:请参阅下图
ApiAuthorizeAttribute 类的代码片段
public class ApiAuthorizeAttribute : AuthorizeAttribute
{
private readonly Entities _entities = new Entities();
private readonly IUserRepository _userRepository = new UserRepository();
public override void OnAuthorization(HttpActionContext filterContext)
{
if (Authorize(filterContext))
{
return;
}
HandleUnauthorizedRequest(filterContext);
}
protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
}
//Here Getting the token from request header and decrypting real information which hidden in token key
private bool Authorize(HttpActionContext actionContext)
{
try
{
var encodedString = actionContext.Request.Headers.GetValues("Token").FirstOrDefault();
bool validFlag = false;
if (!string.IsNullOrEmpty(encodedString))
{
var key = PasswordHash.DecryptText(encodedString);
string[] parts = key.Split('|');
Type myType = typeof(WebApiConfig);
var myNamespace = myType.Namespace;
string protocol = HttpContext.Current.Request.IsSecureConnection ? "https://" : "http://";
string myApiUrl = protocol + HttpContext.Current.Request.Url.Authority;
var userId = Convert.ToInt32(parts[0]); // UserID
var randomKey = parts[1]; // Random Key
var nameSpace = parts[2]; // NameSpace
var apiUrl = parts[3]; // apiUrl
long ticks = long.Parse(parts[4]); // Ticks
var issuedOn = new DateTime(ticks);
var userInfo = _userRepository.GetUserById(userId);
if (userInfo != null && myNamespace==nameSpace && myApiUrl==apiUrl)
{
// Validating Time
var expiresOn = (from token in _entities.TokenManagers
where token.UserId == userId && token.TokenKey == encodedString
select token.ExpiresOn).FirstOrDefault();
validFlag = (DateTime.Now <= expiresOn);
}
}
return validFlag;
}
catch (Exception ex)
{
return false;
}
}
}
步骤 4
您的令牌将持续多长时间? 您必须在 Web.Config 文件中的 <appSettings></appSettings>
XML 标签下定义它。 我将令牌过期时间设置为 45 分钟。 您可以根据您的要求进行自定义。 请参阅以下代码。
令牌过期的代码片段
<appSettings>
<add key="TokenExpiry" value="45" />
</appSettings>
步骤 5
您的新创建的项目中将有一个 Model 文件夹。 添加以下 JSON 格式化程序类,它将序列化 JSON 数据。 该类将在 web API 控制器类中使用。
Json 格式化程序的代码片段
public static class RequestFormat
{
public static JsonMediaTypeFormatter JsonFormaterString()
{
var formatter = new JsonMediaTypeFormatter();
var json = formatter.SerializerSettings;
json.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;
json.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
json.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
json.ContractResolver = new CamelCasePropertyNamesContractResolver();
return formatter;
}
}
在 Model 文件夹下添加另一个类,该类将用于每个 API 响应模型。
API 响应模型的代码片段
// This Class will be used for every API Response It's a response format actually
public class Confirmation
{
public string ResponseStatus { get; set; }
public string Message { get; set; }
public object ResponseData { get; set; }
}
在 Model 文件夹下添加三个(3)个子文件夹,并使用以下命名约定:请参阅下图
1. IRepository
在 IRepository 子文件夹中添加以下代码,并使用单独的 IInterface 文件
public interface IEmployeeRepository
{
object GetAllEmployees();
}
public interface IUserRepository
{
User GetUserById(int userId);
User GetUserByLoginName(string userName);
}
internal interface ILoginRepository
{
object LoginInformation(string userName, string password);
bool IsTokenAlreadyExists(long userId);
int DeleteGenerateToken(long userId);
int InsertToken(TokenManager token);
string GenerateToken(long userId, System.DateTime issuedOn);
}
2. Repository
在 Repository 子文件夹中添加以下代码,每个实现都应该有一个单独的类文件
//Implementation of IEmployeeRepository
public class EmployeeRepository : IEmployeeRepository
{
private readonly Entities _entities;
public EmployeeRepository()
{
this._entities = new Entities();
}
public object GetAllEmployees()
{
try
{
var employee = (from emp in _entities.Employees
select new
{
EmployeeId = emp.EmployeeId,
EmployeeName = emp.EmployeeName,
EmployeeEmail = emp.EmployeeEmail,
EmployeeMobileNumber = emp.EmployeeMobileNumber,
EmployeeAddress = emp.EmployeeAddress
}).OrderByDescending(e => e.EmployeeId).ToList();
return employee;
}
catch (Exception)
{
throw;
}
}
}
//Implementation of IUserRepository
public class UserRepository : IUserRepository
{
private readonly Entities _entities;
public UserRepository()
{
this._entities = new Entities();
}
public User GetUserById(int userId)
{
var user = _entities.Users.Find(userId);
return user;
}
public User GetUserByLoginName(string userName)
{
try
{
var userInfo = _entities.Users.FirstOrDefault(x => x.LoginName == userName);
return userInfo;
}
catch (Exception ex)
{
return null;
}
}
}
//Implementation of ILoginRepository
public class LoginRepository : ILoginRepository
{
private readonly Entities _entities;
private readonly IUserRepository _userRepository;
public LoginRepository()
{
this._entities = new Entities();
this._userRepository = new UserRepository();
}
public object LoginInformation(string userName, string password)
{
try
{
var checkIsUserExists =
_entities.Users.FirstOrDefault(x => x.LoginName == userName && x.PasswordNo == password);
if (checkIsUserExists != null)
{
LoginModel login = new LoginModel();
login.UserId = checkIsUserExists.UserId;
login.LoginName = checkIsUserExists.LoginName;
login.PasswordNo = checkIsUserExists.PasswordNo;
login.FullName = checkIsUserExists.FullName;
return login;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
public bool IsTokenAlreadyExists(long userId)
{
try
{
var result = (from token in _entities.TokenManagers
where token.UserId == userId
select token).Count();
if (result > 0)
{
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
return false;
}
}
public int DeleteGenerateToken(long userId)
{
try
{
var token = _entities.TokenManagers.SingleOrDefault(x => x.UserId == userId);
if (token != null) _entities.TokenManagers.Remove(token);
return _entities.SaveChanges();
}
catch (Exception ex)
{
throw;
}
}
public int InsertToken(TokenManager token)
{
try
{
_entities.TokenManagers.Add(token);
return _entities.SaveChanges();
}
catch (Exception ex)
{
throw;
}
}
public string GenerateToken(long userId, DateTime issuedOn)
{
try
{
Type myType = typeof(WebApiConfig);
var myNamespace = myType.Namespace;
string protocol = HttpContext.Current.Request.IsSecureConnection ? "https://" : "http://";
string apiUrl = protocol + HttpContext.Current.Request.Url.Authority;
string randomnumber =
string.Join("|", new string[]{
Convert.ToString(userId),
KeyGenerator.GetUniqueKey(),
myNamespace,
apiUrl,
Convert.ToString(issuedOn.Ticks)
});
return PasswordHash.EncryptText(randomnumber);
}
catch (Exception ex)
{
throw;
}
}
}
// Class Token Algorithm
public static class KeyGenerator
{
//Here the algorithm of how token will be generated
public static string GetUniqueKey(int maxSize = 15)
{
var chars = new char[62];
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
var data = new byte[1];
using (var crypto = new RNGCryptoServiceProvider())
{
data = new byte[maxSize];
crypto.GetNonZeroBytes(data);
}
var result = new StringBuilder(maxSize);
foreach (byte b in data)
{
result.Append(chars[b % (chars.Length)]);
}
return result.ToString();
}
}
// Class Token Hash Algorithm
public class PasswordHash
{
/// <summary>
/// Functions: This class generates hashes for password and
// verify hashed password with hashed password saved in database,
/// Verification process purposefully delays to give the hacker a lot of pain
/// </summary>
static Random rnd = new Random();
public const int SaltByteSize = 24;
public const int HashByteSize = 20; // to match the size of the PBKDF2-HMAC-SHA-1 hash
// public static int Pbkdf2Iterations = rnd.Next(2000, 3000); // Maruf: 21.Jun.2017
public const int IterationIndex = 0;
public const int SaltIndex = 1;
public const int Pbkdf2Index = 2;
public static string HashPassword(string password)
{
try
{
int pbkdf2Iterations = rnd.Next(2000, 3000);
var cryptoProvider = new RNGCryptoServiceProvider();
var salt = new byte[SaltByteSize];
cryptoProvider.GetBytes(salt);
var hash = GetPbkdf2Bytes(password, salt, pbkdf2Iterations, HashByteSize);
return pbkdf2Iterations + "|" +
Convert.ToBase64String(salt) + "|" +
Convert.ToBase64String(hash);
}
catch (Exception ex)
{
throw;
}
}
public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
{
byte[] encryptedBytes = null;
// Set your salt here, change it to meet your flavor:
// The salt bytes must be at least 8 bytes.
byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cs.Close();
}
encryptedBytes = ms.ToArray();
}
}
return encryptedBytes;
}
public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
{
byte[] decryptedBytes = null;
// Set your salt here, change it to meet your flavor:
// The salt bytes must be at least 8 bytes.
byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
cs.Close();
}
decryptedBytes = ms.ToArray();
}
}
return decryptedBytes;
}
public static string EncryptText(string input, string password = "E6t187^D43%F")
{
// Get the bytes of the string
byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
// Hash the password with SHA256
passwordBytes = SHA256.Create().ComputeHash(passwordBytes);
byte[] bytesEncrypted = AES_Encrypt(bytesToBeEncrypted, passwordBytes);
string result = Convert.ToBase64String(bytesEncrypted);
return result;
}
public static string DecryptText(string input, string password = "E6t187^D43%F")
{
// Get the bytes of the string
byte[] bytesToBeDecrypted = Convert.FromBase64String(input);
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
passwordBytes = SHA256.Create().ComputeHash(passwordBytes);
byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);
string result = Encoding.UTF8.GetString(bytesDecrypted);
return result;
}
public static bool ValidatePassword(string password, string correctHash)
{
try
{
char[] delimiter = { '|' };
var split = correctHash.Split(delimiter);
var iterations = Int32.Parse(split[IterationIndex]);
var salt = Convert.FromBase64String(split[SaltIndex]);
var hash = Convert.FromBase64String(split[Pbkdf2Index]);
var testHash = GetPbkdf2Bytes(password, salt, iterations, hash.Length);
return SlowEquals(hash, testHash);
}
catch (Exception ex)
{
throw;
}
}
private static bool SlowEquals(byte[] a, byte[] b)
{
try
{
var diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
{
diff |= (uint)(a[i] ^ b[i]);
}
return diff == 0;
}
catch (Exception ex)
{
throw;
}
}
private static byte[] GetPbkdf2Bytes(string password, byte[] salt, int iterations, int outputBytes)
{
try
{
var pbkdf2 = new Rfc2898DeriveBytes(password, salt) { IterationCount = iterations };
return pbkdf2.GetBytes(outputBytes);
}
catch (Exception ex)
{
throw;
}
}
}
3. StronglyTypeModel
在 StronglyTypeModel 子文件夹中添加以下代码,每个类都应该有一个单独的类文件
//For Employee Will be call from Web API controller Class
public class EmployeeUserModel
{
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
public string EmployeeEmail { get; set; }
public string EmployeeMobileNumber { get; set; }
public string EmployeeAddress { get; set; }
public int UserId { get; set; }
public string FullName { get; set; }
public string UserName { get; set; }
public string PasswordNo { get; set; }
}
// For Login Will be call from Web API controller Class
public class LoginModel
{
public int UserId { get; set; }
public string FullName { get; set; }
public string LoginName { get; set; }
public string PasswordNo { get; set; }
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
}
步骤 6
现在通过右键单击 Model 文件夹创建一个 ADO.NET 实体数据模型,将模型名称命名为“DbModel
”,在提供所有服务器凭据后,将实体名称(connectionString
名称)命名为“Entity
” 必须使用指定的命名约定。 请参阅下图:
步骤 7
现在按照以下说明在 controller 文件夹下添加 Web API Controller
类
Web API Controller 类的代码片段
请注意,您将从 API 响应标头部分获取您的令牌。 我已将 POST MAN 用于此请求。 您可以在 POST MAN 响应标头部分中找到您生成的令牌。
// For Employee Controller
public class EmployeeController : ApiController
{
private readonly IEmployeeRepository _employeeRepository;
public EmployeeController()
{
this._employeeRepository = new EmployeeRepository();
}
public EmployeeController(IEmployeeRepository employeeRepository)
{
this._employeeRepository = employeeRepository;
}
//Can not access data unknown user without Valid Token
[ApiAuthorize] // use this annotation for authentication I have used only here
[HttpGet, ActionName("GetAllEmployeesWithToken")]
public HttpResponseMessage GetAllEmployeesWithToken()
{
var data = _employeeRepository.GetAllEmployees();
var formatter = RequestFormat.JsonFormaterString();
return Request.CreateResponse(HttpStatusCode.OK, data, formatter);
}
//Can access data unknown user without Valid Token
[AllowAnonymous]
[HttpGet, ActionName("GetAllEmployeesWithOutToken")]
public HttpResponseMessage GetAllEmployeesWithOutToken()
{
var data = _employeeRepository.GetAllEmployees();
var formatter = RequestFormat.JsonFormaterString();
return Request.CreateResponse(HttpStatusCode.OK, data, formatter);
}
}
// For Login Controller
public class LoginController : ApiController
{
private readonly ILoginRepository _loginRepository;
private readonly IUserRepository _userRepository;
public LoginController()
{
_userRepository = new UserRepository();
this._loginRepository = new LoginRepository();
}
[HttpPost, ActionName("UserLogin")]
public HttpResponseMessage UserLogin([FromBody] Models.StronglyType.EmployeeUserModel objEmployeeUserModel)
{
try
{
var formatter = RequestFormat.JsonFormaterString();
if (string.IsNullOrEmpty(objEmployeeUserModel.UserName))
{
return Request.CreateResponse(HttpStatusCode.NotAcceptable,
new Confirmation { ResponseStatus = "error",
Message = "User Name can not be empty" }, formatter);
}
if (string.IsNullOrEmpty(objEmployeeUserModel.PasswordNo))
{
return Request.CreateResponse(HttpStatusCode.NotAcceptable,
new Confirmation { ResponseStatus = "error",
Message = "password can not be empty" }, formatter);
}
var userInfo = _userRepository.GetUserByLoginName(objEmployeeUserModel.UserName);
if (userInfo != null)
{
var login = _loginRepository.LoginInformation
(objEmployeeUserModel.UserName, objEmployeeUserModel.PasswordNo);
if (login != null)
{
var oResponse = Request.CreateResponse(HttpStatusCode.OK,
new Confirmation { ResponseStatus = "success",
Message = "Login Successfully", ResponseData = userInfo }, formatter);
if (_loginRepository.IsTokenAlreadyExists(userInfo.UserId))
{
_loginRepository.DeleteGenerateToken(userInfo.UserId);
return GenerateandSaveToken(userInfo.UserId, oResponse);
}
else
{
return GenerateandSaveToken(userInfo.UserId, oResponse);
}
}
return Request.CreateResponse(HttpStatusCode.Forbidden,
new Confirmation { ResponseStatus = "error",
Message = "Please enter valid username or password" }, formatter);
}
return Request.CreateResponse(HttpStatusCode.Forbidden,
new Confirmation { ResponseStatus = "error",
Message = "Please enter valid username or password" }, formatter);
}
catch (Exception ex)
{
var formatter = RequestFormat.JsonFormaterString();
return Request.CreateResponse(HttpStatusCode.OK, new Confirmation
{ ResponseStatus = "error",
Message = "Login is not successful" }, formatter);
}
}
[NonAction]
private HttpResponseMessage GenerateandSaveToken(int userId, HttpResponseMessage response)
{
try
{
var issuedOn = DateTime.Now;
var newToken = _loginRepository.GenerateToken(userId, issuedOn);
var token = new TokenManager();
token.TokenID = 0;
token.TokenKey = newToken;
token.IssuedOn = issuedOn;
token.ExpiresOn = DateTime.Now.AddMinutes(Convert.ToInt32
(ConfigurationManager.AppSettings["TokenExpiry"]));
token.CreatedOn = DateTime.Now;
token.UserId = userId;
var result = _loginRepository.InsertToken(token);
if (result == 1)
{
response.Headers.Add("Token", newToken);
response.Headers.Add("TokenExpiry",
ConfigurationManager.AppSettings["TokenExpiry"]);
response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry");
return response;
}
var message = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
message.Content = new StringContent("Error in Creating Token");
return message;
}
catch (Exception ex)
{
var formatter = RequestFormat.JsonFormaterString();
return Request.CreateResponse(HttpStatusCode.InternalServerError,
new Confirmation { ResponseStatus = "error",
Message = "Cannot generate and Save Token" }, formatter);
}
}
}
步骤 7
正如您在上面看到的,我没有在这里使用 RESTful API,因此当您尝试从 Post Man 发送请求时,您可能会收到 404 错误。 要解决此问题,请将以下代码粘贴到 App_Start 文件夹下的 WebApiConfig.cs 文件中,以便它允许您在 controller
类中使用多个 http 动词。
WebApiConfig.cs 的代码片段
config.Routes.MapHttpRoute(
name: "ControllersWithAction",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
最后一步
只需构建您的项目并从 Post Man 发送请求,并使用 JSON 数据格式。 请参阅下图
Post Man 请求格式
Post Man 响应格式,带令牌
json 请求格式的代码片段
{
"UserName": "admin",
"PasswordNo": "123456"
}
通过 POST MAN 以 json 请求格式访问您的数据:请参阅下图:当您尝试访问 controller
类时,在 post man 请求标头部分中添加您的令牌
关注点
当我尝试写任何主题时,我都会感到一种美妙的兴奋。 我必须经历一系列的研发。 最后,我在我的思想中绘制了一个格式。 我希望将我的知识传播给技术爱好者。 这真的让我感觉很好。 我总是尝试以简单易懂的方式描述技术。