我的第一个微服务系列(三):搭建IdentityServer服务器

  本篇介绍如何通过IdentityServer服务器调用User.Api来验证用户信息。

  新建User.Identity项目,引入Nuget包:IdentityServer4,关于IdentityServer4的相关知识可以参考另一篇博文:https://www.cnblogs.com/jesen1315/p/11427294.html 。

  在此项目中,将使用手机号和验证码来作为注册或登录验证的判断,这需要调用到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
{
    /// <summary>
    /// 自定义验证码授权验证
    /// </summary>
    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 IEnumerable<Client> GetClients()
        {
            return new List<Client> {
                new Client()
                {
                    ClientId ="android",
                    ClientSecrets = new List<Secret>
                    {
                        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<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource> {
                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可能部署的不只一台而是多台,这个时候就需要引入服务注册和发现这种机制来解决,我们下篇再来讨论。

  

posted @ 2020-12-07 22:47  柠檬笔记  阅读(341)  评论(0编辑  收藏  举报