• 我的微服务项目之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,如果有不合理或错误的地方希望大家指出,欢迎大家访问我的站点:天天博客

  • 相关阅读:
    ubuntu 安装eclipse
    java中的ArrayList 、List、LinkedList、Collection关系详解
    JERSEY中文翻译(第三章、模块和依赖)
    JERSEY中文翻译(第一章、Getting Started、2.2)
    JERSEY中文翻译(第三章、JAX-RS Application, Resources and Sub-Resources)
    JERSEY中文翻译(第一章、Getting Started、1.1.7)
    创建一个入门的JAVA WEB站点(REST JERSEY)
    一个JAVA的WEB服务器事例
    一个简单的C/S事例——JAVA-Socket
    FILTER——JAVA
  • 原文地址:https://www.cnblogs.com/MrHanBlog/p/14334155.html
Copyright © 2020-2023  润新知