认证服务用来验证用户登录并颁发JWT,也提供用户管理等API。认证服务的实现代码都基于Authentication与Authorization。
开发认证服务的领域层
IdentityService.Domain是认证服务的领域层项目。ASP.NET Core的标识框架中已经提供了IdentityRole、IdentityUser等基础的实体类,我们只要编写它们的子类,然后根据需要再添加自定义的属性即可。
首先创建Role和User类进行编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| namespace IdentityService.Domain.Entities { public class Role : IdentityRole<Guid> { public Role() { this.Id = Guid.NewGuid(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| namespace IdentityService.Domain.Entities { public class User : IdentityUser<Guid>, IHasCreationTime, IHasDeletionTime, ISoftDelete { public DateTime CreationTime { get; init; }
public DateTime? DeletionTime { get; private set; }
public bool IsDeleted { get; private set; }
public User(string userName) : base(userName) { Id = Guid.NewGuid(); CreationTime = DateTime.Now; }
public void SoftDelete() { this.IsDeleted = true; this.DeletionTime = DateTime.Now; } } }
|
在根据手机号加短信验证码进行登录的时候,我们需要实现发送短信的功能,而短信验证码发送服务的提供商也比较多,为了屏蔽不同提供商的代码,我们开发了一个防腐层接口ISmsSender。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| namespace IdentityService.Domain { public interface ISmsSender { public Task SendAsync(string phoneNum, params string[] args); } }
|
接下来,我们定义仓储接口IIdRepository,这个接口提供了很多方法,比如:根据Id获取用户,重置密码,删除密码等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| namespace IdentityService.Domain { public interface IIdRepository { Task<User?> FindByIdAsync(Guid id);
Task<User?> FindByNameAsync(string userName);
Task<User?> FindByPhoneNumberAsync(string phoneNum);
Task<IdentityResult> CreateAsync(User user, string password);
Task<IdentityResult> AccessFaailedAsync(User user);
Task<string> GenerateChangePhoneNumberTokenAsync(User user, string phoneNumber);
Task<SignInResult> ChangePasswordAsync(Guid userId, string phoneNum, string token);
Task<IdentityResult> ChangePasswordAsync(Guid userId, string password);
Task<IList<string>> GetRolesAsync(User user);
Task<IdentityResult> AddToRoleAsync(User user, string role);
public Task<SignInResult> CheckForSignInAsync(User user, string password, bool lockoutOnFailure);
public Task ConfirmPhoneNumberAsync(Guid id);
public Task UpdatePhoneNumberAsync(Guid id, string phoneNum);
public Task<IdentityResult> RemoveUserAsync(Guid id);
public Task<(IdentityResult, User?, string? password)> AddAdminUserAsync(string userName, string phoneNum);
public Task<(IdentityResult, User?, string? password)> ResetPasswordAsync(Guid id); } }
|
最后,我们开发认证领域服务IdDomainService。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| namespace IdentityService.Domain { public class IdDomainService { private readonly IIdRepository repository; private readonly ITokenService tokenService; private readonly IOptions<JWTOptions> optJWT;
public IdDomainService(IIdRepository repository,ITokenService tokenService,IOptions<JWTOptions> optJWT) { this.repository = repository; this.tokenService = tokenService; this.optJWT = optJWT; }
private async Task<SignInResult> CheckUserNameAndPwdAsync(string userName,string password) { var user = await repository.FindByNameAsync(userName); if (user == null) { return SignInResult.Failed; } var result= await repository.CheckForSignInAsync(user, password,true); return result; }
private async Task<SignInResult> CheckPhoneNumAndPwdAsync(string phoneNum,string password) { var user = await repository.FindByPhoneNumberAsync(phoneNum); if(user == null) { return SignInResult.Failed; } var result=await repository.CheckForSignInAsync(user, password,true); return result; }
public async Task<(SignInResult Result, string? Token)> LoginByPhoneAndPwdAsync(string phoneNum, string password) { var checkResult = await CheckPhoneNumAndPwdAsync(phoneNum, password); if (checkResult.Succeeded) { var user = await repository.FindByPhoneNumberAsync(phoneNum); string token = await BuildTokenAsync(user); return (SignInResult.Success,token); } else { return (checkResult, null); } }
public async Task<(SignInResult Result,string? Token)> LoginByUserNameAndPwdAsync(string userName,string password) { var checkResult=await CheckUserNameAndPwdAsync(userName,password); if (checkResult.Succeeded) { var user=await repository.FindByNameAsync(userName); string token=await BuildTokenAsync(user); return (SignInResult.Success, token); } else { return (checkResult, null); } }
private async Task<string> BuildTokenAsync(User user) { var roles=await repository.GetRolesAsync(user); List<Claim> claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier,user.Id.ToString())); foreach(string role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } return tokenService.BuildToken(claims, optJWT.Value); } } }
|
LoginByUserNameAndPwdAsync方法根据用户名、密码进行登录。这个方法需要返回是否执行成功和登录成功后生成的JWT两个值。
开发认证服务的基础设施层
认证服务的基础设施层项目IdentityService.Infrastructure,提供模板通过手机号进行短信发送,方便开发,我们用日志输出来模拟短信发送。
开发一个MockSmsSender类,它用日志输出来模拟短信发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| namespace IdentityService.Infrastructure.Services { public class MockEmailSender : IEmailSender { private readonly ILogger<MockEmailSender> logger;
public MockEmailSender(ILogger<MockEmailSender> logger) { this.logger = logger; } public Task SendAsync(string toEmail, string subject, string body) { logger.LogInformation("Send Email to {0},title:{1},body:{2}", toEmail, subject, body); return Task.CompletedTask; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| namespace IdentityService.Infrastructure.Services { public class MockSmsSender : ISmsSender { private readonly ILogger<MockSmsSender> logger;
public MockSmsSender(ILogger<MockSmsSender> logger) { this.logger = logger; }
public Task SendAsync(string phoneNum, params string[] args) { logger.LogInformation("Send SMS to {0},args:{1}", phoneNum, string.Join(",", args)); return Task.CompletedTask; } } }
|