forked from ddrilling/AsbCloudServer
228 lines
7.4 KiB
C#
228 lines
7.4 KiB
C#
using AsbCloudApp.Data.User;
|
||
using AsbCloudApp.Exceptions;
|
||
using AsbCloudApp.Repositories;
|
||
using AsbCloudApp.Services;
|
||
using AsbCloudDb.Model;
|
||
using Mapster;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.IdentityModel.Tokens;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IdentityModel.Tokens.Jwt;
|
||
using System.Linq;
|
||
using System.Security.Claims;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
|
||
namespace AsbCloudInfrastructure.Services;
|
||
|
||
/// <inheritdoc/>
|
||
public class AuthService : IAuthService
|
||
{
|
||
private readonly IAsbCloudDbContext db;
|
||
private readonly IUserRepository userRepository;
|
||
|
||
public const string issuer = "a";
|
||
public const string audience = "a";
|
||
public static readonly SymmetricSecurityKey securityKey = new(Encoding.ASCII.GetBytes("супер секретный ключ для шифрования"));
|
||
private const string algorithms = SecurityAlgorithms.HmacSha256;
|
||
|
||
private static readonly TimeSpan expiresTimespan = TimeSpan.FromDays(365.25);
|
||
private static readonly Encoding encoding = Encoding.UTF8;
|
||
private const int PasswordSaltLength = 5;
|
||
private const string claimIdUser = "id";
|
||
private const string claimNameIdCompany = "idCompany";
|
||
private readonly HashAlgorithm hashAlgorithm;
|
||
private readonly Random rnd;
|
||
|
||
public AuthService(IAsbCloudDbContext db, IUserRepository userRepository)
|
||
{
|
||
this.db = db;
|
||
this.userRepository = userRepository;
|
||
hashAlgorithm = SHA384.Create();
|
||
rnd = new Random((int)(DateTime.Now.Ticks % 2147480161));
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public async Task<UserTokenDto?> LoginAsync(string login, string password,
|
||
CancellationToken token)
|
||
{
|
||
var user = await GetUserByLoginAsync(login, token);
|
||
if (user is null)
|
||
return null;
|
||
|
||
if (!CheckPassword(user.PasswordHash, password))
|
||
return null;
|
||
|
||
return await MakeUserTokenDto(user, token);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public async Task<UserTokenDto?> RefreshAsync(ClaimsPrincipal identity,
|
||
CancellationToken token)
|
||
{
|
||
var login = identity.FindFirst(ClaimsIdentity.DefaultNameClaimType)?.Value;
|
||
if (string.IsNullOrEmpty(login))
|
||
return null;
|
||
|
||
var user = await GetUserByLoginAsync(login, token);
|
||
if (user is null)
|
||
return null;
|
||
|
||
var dto = await MakeUserTokenDto(user, token);
|
||
return dto;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void Register(UserRegistrationDto userDto)
|
||
{
|
||
var user = db.Users.FirstOrDefault(u => u.Login == userDto.Login)
|
||
?? throw new ArgumentInvalidException(nameof(userDto.Login), "Логин уже занят");
|
||
|
||
var salt = GenerateSalt();
|
||
|
||
var newUser = new User
|
||
{
|
||
IdCompany = userDto.IdCompany,
|
||
IdState = 0,
|
||
Name = userDto.Name,
|
||
Surname = userDto.Surname,
|
||
Patronymic = userDto.Patronymic,
|
||
Email = userDto.Email,
|
||
Phone = userDto.Phone,
|
||
Position = userDto.Position,
|
||
Login = userDto.Login,
|
||
PasswordHash = salt + ComputeHash(salt, userDto.Password),
|
||
};
|
||
|
||
db.Users.Add(newUser);
|
||
db.SaveChanges();
|
||
db.RelationUserUserRoles.Add(new RelationUserUserRole
|
||
{
|
||
IdUser = newUser.Id,
|
||
IdUserRole = 2
|
||
});
|
||
db.SaveChanges();
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void ChangePassword(string userLogin, string newPassword)
|
||
{
|
||
var user = db.Users.FirstOrDefault(u => u.Login == userLogin)
|
||
?? throw new ArgumentInvalidException(nameof(userLogin), "Логин не зарегистрирован");
|
||
|
||
var salt = GenerateSalt();
|
||
user.PasswordHash = salt + ComputeHash(salt, newPassword);
|
||
db.SaveChanges();
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void ChangePassword(int idUser, string newPassword)
|
||
{
|
||
var user = db.Users.FirstOrDefault(u => u.Id == idUser)
|
||
?? throw new ArgumentInvalidException(nameof(idUser), $"Пользователь с idUser:{idUser} не зарегистрирован");
|
||
|
||
var salt = GenerateSalt();
|
||
user.PasswordHash = salt + ComputeHash(salt, newPassword);
|
||
db.SaveChanges();
|
||
}
|
||
|
||
private async Task<UserTokenDto?> MakeUserTokenDto(User user, CancellationToken token)
|
||
{
|
||
var identity = MakeClaims(user);
|
||
if (identity is null || user.IdState == 0)
|
||
return null;
|
||
|
||
var userDto = await userRepository.GetOrDefaultAsync(user.Id, token);
|
||
if (userDto is null)
|
||
return null;
|
||
|
||
var dto = userDto.Adapt<UserTokenDto>();
|
||
dto.Permissions = userRepository.GetNestedPermissions(userDto.Id);
|
||
dto.Token = MakeToken(identity.Claims);
|
||
return dto;
|
||
}
|
||
|
||
private static string MakeToken(IEnumerable<Claim> claims)
|
||
{
|
||
var now = DateTime.Now;
|
||
|
||
var jwt = new JwtSecurityToken(
|
||
issuer,
|
||
audience,
|
||
notBefore: now,
|
||
claims: claims,
|
||
expires: now.Add(expiresTimespan),
|
||
signingCredentials: new SigningCredentials(securityKey, algorithms));
|
||
|
||
return new JwtSecurityTokenHandler().WriteToken(jwt);
|
||
}
|
||
|
||
private async Task<User?> GetUserByLoginAsync(string login, CancellationToken token = default)
|
||
{
|
||
var user = await db.Users
|
||
.Include(e => e.Company)
|
||
.Where(e => e.Login == login)
|
||
.AsNoTracking()
|
||
.FirstOrDefaultAsync(token)
|
||
.ConfigureAwait(false);
|
||
return user;
|
||
}
|
||
|
||
private ClaimsIdentity MakeClaims(User user)
|
||
{
|
||
var claims = new List<Claim>
|
||
{
|
||
new (claimIdUser, user.Id.ToString()),
|
||
new (ClaimsIdentity.DefaultNameClaimType, user.Login),
|
||
new (claimNameIdCompany, user.IdCompany.ToString()),
|
||
};
|
||
var roles = userRepository.GetRolesByIdUser(user.Id);
|
||
if (roles is not null)
|
||
foreach (var role in roles)
|
||
claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, role.Caption));
|
||
|
||
var claimsIdentity = new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
|
||
return claimsIdentity;
|
||
}
|
||
|
||
private bool CheckPassword(string passwordHash, string password)
|
||
{
|
||
if (passwordHash?.Length == 0 && password.Length == 0)
|
||
return true;
|
||
|
||
if (passwordHash?.Length < PasswordSaltLength)
|
||
return false;
|
||
|
||
if (passwordHash is null)
|
||
return false;
|
||
|
||
var salt = passwordHash[0..PasswordSaltLength];
|
||
var hashDb = passwordHash[PasswordSaltLength..];
|
||
|
||
return hashDb == ComputeHash(salt, password);
|
||
}
|
||
|
||
private string ComputeHash(string salt, string password)
|
||
{
|
||
var hashBytes = hashAlgorithm.ComputeHash(encoding.GetBytes(salt + password));
|
||
var hashString = BitConverter.ToString(hashBytes)
|
||
.Replace("-", "")
|
||
.ToLower();
|
||
return hashString;
|
||
}
|
||
|
||
private string GenerateSalt()
|
||
{
|
||
const string saltChars = "sHwiaX7kZT1QRp0cPILGUuK2Sz=9q8lmejDNfoYCE3B_WtgyVv6M5OxAJ4Frbhnd";
|
||
var salt = "";
|
||
for (var i = 0; i < PasswordSaltLength - 1; i++)
|
||
salt += saltChars[rnd.Next(0, saltChars.Length)];
|
||
salt += "|";
|
||
return salt;
|
||
}
|
||
}
|