我的微服务项目之IdentityServer4

 2021,祝大家新年快乐!!!

 

     2021年了,新的一年应该有新的计划,我的计划是准备去学习微服务,所以我将我自己的博客项目拆分成了一个微服务项目,用来给自己学习,项目地址:http://www.ttblog.site/。之前做的一个认证服务比较简单,只是单纯的生成jwtToken,所以想改造下。看了很多资料,发现了.net下的一个基于 OpenID Connect 和 OAuth 2.0 认证框架:IdentityServer4,所以就查了一下资料就用在了我的项目上。因为我也是刚刚学习IdentityServer4,所以如果有说错的地方希望大家指出。

    我的想法是不想让我的api裸奔,所以要加个认证,不是谁都可以调用我的apide1,这里我选择了IdenttiyServer4的客户端凭据许可模式(client_credentials),这里推荐一个文章:https://www.cnblogs.com/ddrsql/p/7789064.html。因为我觉得这种模式正好符合自己的要求,对于其它的模式我也只是了解过,但是并没有实践过。

   第一步:搭建一个IdentityServer服务器

  

添加IdentityServer4的nuget包。然后我添加了一个idenityserver.json的文件用来配置一些认证资源

{
  "WebApi": {
    "ApiResource": "",
"ApiScope": "", "Client": { "ClientId": "", "ClientName": "", "ClientSecrets": "", "AllowedGrantTypes": "", "AllowedScopes": "", "AccseeTokenTime": "" //单位秒 } } }

  然后添加ApiConfig类:

using Core.Configuration;
using IdentityServer4;
using IdentityServer4.Models;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static IdentityServer4.IdentityServerConstants;

namespace Core.Auth.IdentityServer4
{
    public class ApiConfig
    {
        public static IConfiguration Configuration = ConfigureProvider.configuration;
        private static string[] ApiNames = { "WebApi" };
        /// <summary>
        /// Define which APIs will use this IdentityServer
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> apiResources = new List<ApiResource>();
            foreach (var item in ApiNames)
            {
                var configuration = Configuration.GetSection(item);
                string name = configuration.GetSection("ApiResource").Value;
                string[] scopes = configuration.GetSection("ApiScope").Value.Split(",");
                ApiResource apiResource = new ApiResource(name, name) {
                    Scopes = scopes
                };
                apiResources.Add(apiResource);
            }
            return apiResources;
        }
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            List<ApiScope> apiScopes = new List<ApiScope>();
            foreach (var item in ApiNames)
            {
                var configuration = Configuration.GetSection(item);
                string[] names = configuration.GetSection("ApiScope").Value.Split(",");
                foreach(var name in names)
                {
                    ApiScope apiScope = new ApiScope(name, name);
                    apiScopes.Add(apiScope);
                }
            }
            return apiScopes;
        }
        /// <summary>
        /// Define which Apps will use thie IdentityServer
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>();
            foreach(var item in ApiNames)
            {
                var configuration = Configuration.GetSection(item);
                string clientId = configuration["Client:ClientId"];
                string clientName = configuration["Client:ClientName"];
                Secret[] secrets = configuration["Client:ClientSecrets"].Split(',').Select(s=>new Secret(s.Sha256())).ToArray() ;
                string[] grantTypes = configuration["Client:AllowedGrantTypes"].Split(",");
                List<string> allowedScopes = configuration["Client:AllowedScopes"].Split(',').ToList();
                //allowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
                //allowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
                allowedScopes.Add(StandardScopes.OfflineAccess);
                int accseeTokenTime =Convert.ToInt32(configuration["Client:AccseeTokenTime"]);
                Client client = new Client();
                client.ClientId = clientId;
                client.ClientName = clientName;
                client.ClientSecrets = secrets;
                client.AllowedGrantTypes = grantTypes;
                client.AllowedScopes = allowedScopes;
                client.AccessTokenLifetime = accseeTokenTime;
                client.AllowOfflineAccess = true;
                clients.Add(client);

            }
            return clients;
        }

        /// <summary>
        /// Define which IdentityResources will use this IdentityServer
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                 new IdentityResources.Profile(),
            };
        }
    }
}

  这里有个问题,我网上百度的很多人写的就是  ApiResource apiResource = new ApiResource(name, name);就好了,但是我这里不行,如果不像下面这样写的话就一直401,然后看控制台提示“

IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[7]
WebApiAuthKey was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'System.String'. Did not match: validationParameters.ValidAudience: 'System.String' or validationParameters.ValidAudiences: 'System.String'.

”不知道是不是因为我集成了Ocelot的原因,也有人说是IdentityServer4的4.0版本之后都要这样写

ApiResource apiResource = new ApiResource(name, name) {
Scopes = scopes
};

  然后开始添加服务认

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;

namespace Core.Auth.IdentityServer4
{
    public static class ConfigureIdentityServer4
    {
        public static IServiceCollection AddIdentityServer4(this IServiceCollection services)
        {
            string basePath = PlatformServices.Default.Application.ApplicationBasePath;
            IIdentityServerBuilder identityServerBuilder = services.AddIdentityServer();
            identityServerBuilder.AddDeveloperSigningCredential();           
            identityServerBuilder.AddInMemoryIdentityResources(ApiConfig.GetIdentityResources())
                .AddInMemoryApiResources(ApiConfig.GetApiResources())
                .AddInMemoryClients(ApiConfig.GetClients())
                .AddInMemoryApiScopes(ApiConfig.GetApiScopes());
            return services;
        }
    }
}

  以上步骤弄完之后,你用postman访问http://localhost:5003/.well-known/openid-configuration,出现一下内容代表搭建成功:

第二步:网关添加认证:

     最开始我想的是每一个服务都取添加注册认证服务,这样当然可以实现,但是这样网关悠悠什么用的。我对微服务的理解是,应该所有的请求通过网关,网关应该是是对外的,任何人都不应该知道具体的服务地址,包括认证授权,网关就应该起到一个对api的保护作用。所以我放弃了这种想法,转而去针对网关实现对认证服务的注册,很幸运,ocelot也支持identityserver,只需要简单的配置,如下部分截图:

然后添加配置节点:

  "IdentityService": {
    "Uri": "http://localhost:5003",//自己的认证服务地址,网关的地址为5000
    "UseHttps": false,
    "ApiName": {
      "WebApi": "WebApi"
    }
  }

  并且在Startup里面添加如下代码:

 #region IdentityServerAuthenticationOptions => need to refactor
            Action<IdentityServerAuthenticationOptions> webOption = option =>
            {
                option.Authority = Configuration["IdentityService:Uri"];
                option.ApiName = Configuration["IdentityService:ApiName:WebApi"];
                option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
            };
            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
           .AddIdentityServerAuthentication("WebApiAuthKey", webOption);

 到此,可以说已经是初步完成了服务的认证,我这里并没有授权操作,所以就不谈授权。

 第三步 :请求获取token

      我们通过postman直接访问http://localhost:5003/connect/token,然后输入必要的参数,具体的操作可以网上百度,当然可以获取token。当是我这里是要结合我自己的项目,其中肯定不想让自己设定的一些关键信息如ClientId和Secret让别人知道,放到js里面,别人直接f12查看源码就知道了,所以我的想法是通过nginx设置header的方式,这样就可以了。如下:

       location /auth/credentials/token {
           proxy_set_header   client_secret   "";
           proxy_set_header  client_id   "";
           proxy_pass  http://127.0.0.1:5000;
       }     
     //我这里是通过Ocelot转发到认证服务的,不要盲目复制

  然后认证服务新加一个api

using Core.Auth;
using Core.Common.EnumExtensions;
using Core.Common.Http;
using IdentityModel.Client;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;

namespace BlogAuthApi
{
    [Route("api/auth")]
    [ApiController]
    public class AuthController: ControllerBase
    {
        private IHttpClientFactory _httpClientFactory;
        public AuthController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
        //[Route("token")]
        //[HttpGet]
        //public ApiResult GetToken()
        //{
        //    DateTime now = DateTime.Now;
        //    var claims = new Claim[]
        //        {
        //            // 声明主题
        //            new Claim(JwtRegisteredClaimNames.Sub, "Blog"),
        //            //JWT ID 唯一标识符
        //            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        //            // 发布时间戳 issued timestamp
        //            new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
        //        };
        //    JwtToken jwtToken = Jwt.GetToken(claims);
        //    return ApiResult.Success(jwtToken);
        //}

        [Route("credentials/token")]
        [HttpGet]
        public async Task<ApiResult> GetToken()
        {
            Request.Headers.TryGetValue("client_id", out StringValues clientId);
            Request.Headers.TryGetValue("client_secret", out StringValues clientSecret);
            HttpClient httpClient= _httpClientFactory.CreateClient();
            Dictionary<string, string> headers = new Dictionary<string, string>();
            headers.Add("client_id", clientId);
            headers.Add("client_secret", clientSecret);
            headers.Add("grant_type", "client_credentials");
            FormUrlEncodedContent content = new FormUrlEncodedContent(headers);
            var response=await httpClient.PostAsync("http://localhost:5003/connect/token",content);
            if (!response.IsSuccessStatusCode)
                return ApiResult.Error(HttpStatusCode.BAD_REQUEST, response.StatusCode.GetEnumText());
            var responseString=await response.Content.ReadAsStringAsync();
            dynamic result = JsonConvert.DeserializeObject<dynamic>(responseString);
            int expires = result.expires_in;
            int minutes = new TimeSpan(0, 0, expires).Minutes;
            string token = result.access_token;
            JwtToken jwtToken = new JwtToken(token, minutes, DateTime.Now);
            return ApiResult.Success(jwtToken);          
        }
    }
}

  这一样,我算是完成了自己的认证保护api的功能。

     接下来我们测试一下,访问接口,我这里nginx代理我本地的80端口

     

获取到token,然后请求自己的接口,成功请求了:

接下来输入一个错误的token,我在token上加上23456,或者不加token,发现直接401了

,

然后过一段时间在访问,因为token已经过期了,所以也会401,我们看控制台输出

 这里有个问题,因为我设置的两分钟过期,但是两分钟过后并不会真的立刻过期,具体的原因参考:https://www.cnblogs.com/stulzq/p/8998274.html.

我还没有具体的用在我的站点上面,因为我还没有弄IdentityServer4的证书,只是我本地用postman测试了下,我也是初步学习IdentityServer4,如果有不合理或错误的地方希望大家指出,欢迎大家访问我的站点:天天博客

posted @ 2021-01-27 13:57  灬丶  阅读(811)  评论(0编辑  收藏  举报