我的第一个微服务系列(三):搭建IdentityServer服务器
本篇介绍如何通过IdentityServer服务器调用User.Api来验证用户信息。
新建User.Identity项目,引入Nuget包:IdentityServer4,关于IdentityServer4的相关知识可以参考另一篇博文: 。
在此项目中,将使用手机号和验证码来作为注册或登录验证的判断,这需要调用到User.Api项目来判断,如果该手机号未注册,则自动完成注册。User.Identity 将通过网关来访问,所有的外部访问请求需要先从User.Identity获取token后再发起,也就是说所有的请求都是经过网关再转发到对应的Api中。
因此,我们需要自定义我们的GrantType,而IdentityServer4中自定义GrantType需要实现 IExtensionGrantValidator 接口。所以先来定义实现IExtensionGrantValidator的类SmsAuthCodeValidator,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using IdentityServer4.Models; using IdentityServer4.Validation; using User.Identity.Services; namespace User.Identity.Authentication { ////// 自定义验证码授权验证 /// public class SmsAuthCodeValidator : IExtensionGrantValidator { private readonly IUserService _userService; private readonly IAuthCodeService _authCodeService; public SmsAuthCodeValidator(IAuthCodeService authCodeService ,IUserService userService) { _authCodeService = authCodeService; _userService = userService; } public string GrantType => "sms_auth_code"; //http 请求时的granttype public async Task ValidateAsync(ExtensionGrantValidationContext context) { var phone = context.Request.Raw["phone"]; var code = context.Request.Raw["auth_code"]; var errorValidationResult = new GrantValidationResult(TokenRequestErrors.InvalidGrant); if(string.IsNullOrWhiteSpace(phone) || string.IsNullOrWhiteSpace(code)) { context.Result = errorValidationResult; return; } // 检查验证码 if (!_authCodeService.Validate(phone, code)) { context.Result = errorValidationResult; return; } // 用户注册 var userInfo = await _userService.CheckOrCreateAsync(phone); if (userInfo == null) { context.Result = errorValidationResult; return; } var claims = new Claim[] { new Claim("name",userInfo.Name??string.Empty), new Claim("company",userInfo.Company??string.Empty), new Claim("title",userInfo.Title??string.Empty), new Claim("avatar",userInfo.Avatar??string.Empty), }; context.Result = new GrantValidationResult(userInfo.Id.ToString(), GrantType,claims); } } }
默认情况下,IdentityServer只能从认证(authentication)Cookie中获取保存的claims信息。但是将用户的所有可用的信息都保存到Cookie中很显然是不现实的,也是一种不好的实践,所以,IdentityServer定义了一个可扩展的接口,允许动态的加载用户的Claim,这个接口就是IProfileService。开发人员通常实现此接口来访问包含用户数据(claims)的自定义数据库或API。IdentityServer4会在每次请求User信息的时候调用实现了IProfileService的类中的GetProfileDataAsync方法,而IProfileService的另一个方法IsActiveAsync,则使用来确定用户是否有效或激活状态。
因此,我们还需要定义一个实现IProfileService的类ProfileService。
using IdentityServer4.Models; using IdentityServer4.Services; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace User.Identity.Authentication { public class ProfileService : IProfileService { public Task GetProfileDataAsync(ProfileDataRequestContext context) { var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject)); var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value; if(!int.TryParse(subjectId,out int intUserId)) { throw new ArgumentException("Invalid subject identifier"); } context.IssuedClaims = context.Subject.Claims.ToList(); return Task.CompletedTask; } public Task IsActiveAsync(IsActiveContext context) { var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject)); var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value; context.IsActive = int.TryParse(subjectId, out int intUserId); return Task.CompletedTask; } } }
完成这两步之后,在之前介绍IdentityServer4的时候知道需要配置Client、IdentityResource、ApiResource来授予受信任的客户端获取信息。
using IdentityServer4; using IdentityServer4.Models; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace User.Identity { public class Config { public static IEnumerableGetClients() { return new List { new Client() { ClientId ="android", ClientSecrets = new List { new Secret("secret".Sha256()) }, RefreshTokenExpiration = TokenExpiration.Sliding, AllowOfflineAccess = true, RequireClientSecret = false, AllowedGrantTypes = new List<string>{"sms_auth_code"}, AlwaysIncludeUserClaimsInIdToken = true, AllowedScopes =new List<string> { "gateway_api", "contact_api", "user_api", "project_api", "recommend_api", IdentityServerConstants.StandardScopes.OfflineAccess, IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } } }; } public static IEnumerable GetIdentityResources() { return new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } public static IEnumerable GetApiResources() { return new List { new ApiResource("gateway_api","User Service"), new ApiResource("contact_api","contact Service"), new ApiResource("user_api","User Service"), new ApiResource("project_api","Project Service"), new ApiResource("recommend_api","Recommend Service") }; } } }
到此,关于IdentityServer的配置基本完成,接下来要做的是如何在User.Identity项目中跨服务调用User.Api项目来验证用户合法性。普通的方式我们是在配置文件中配置User.Api的Url后再代码中使用HttpClient或HttpWebRequest直接调用来验证,但是这种方式在微服务中已经不再适用,因为微服务中User.Api可能部署的不只一台而是多台,这个时候就需要引入服务注册和发现这种机制来解决,我们下篇再来讨论。