(10)學習筆記 ) ASP.NET CORE微服務 Micro-Service ---- Ocelot+Identity Server

用 JWT 機制實現驗證的原理以下圖:  認證服務器負責頒發 Token(至關於 JWT 值)和校驗 Token 的合法性。html

 

 

1、 相關概念

API 資源(API Resource):微博服務器接口、鬥魚彈幕服務器接口、鬥魚直播接口就是API 資源。android

客戶端(Client):Client 就是官方微博 android 客戶端、官方微博 ios 客戶端、第三方微博客戶端、微博助手等。ios

身份資源(Identity Resource):就是用戶。web

一個用戶可能使用多個客戶端訪問服務器;一個客戶端也可能服務多個用戶。封禁了一個客戶端,全部用戶都不能使用這個這個客戶端訪問服務器,可是可使用其餘客戶端訪問;封禁了一個用戶,這個用戶在全部設備上都不能訪問,可是不影響其餘用戶。數據庫

2、 搭建 identity server 認證服務器

新建一個空的 web 項目 ID4.IdServerjson

Nuget - 》 Install-Package IdentityServer4

首先編寫一個提供應用列表、帳號列表的 Config 類後端

using IdentityServer4.Models;
using System.Collections.Generic;
namespace ID4.IdServer
{
    public class Config
    {
        /// <summary>
        /// 返回應用列表
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> resources = new List<ApiResource>();
            //ApiResource第一個參數是應用的名字,第二個參數是描述
            resources.Add(new ApiResource("MsgAPI", "消息服務API"));
            resources.Add(new ApiResource("ProductAPI", "產品API"));
            return resources;
        }
        /// <summary>
        /// 返回帳號列表
        /// </summary>         
        /// <returns></returns>         
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>();
            clients.Add(new Client
            {
                ClientId = "clientPC1",//API帳號、客戶端Id
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                ClientSecrets =
                {
                    new Secret("123321".Sha256())//祕鑰
                },
                AllowedScopes = { "MsgAPI", "ProductAPI" }//這個帳號支持訪問哪些應用
            });
            return clients;
        }
    }
}

 若是容許在數據庫中配置帳號等信息,那麼能夠從數據庫中讀取而後返回這些內容。疑問待解。api

修改Startup.cs服務器

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients());
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseIdentityServer();
}

而後在 9500 端口啓動app

 在 postman 裏發出請求,獲取 token http://localhost:9500/connect/token,發 Post 請求,表單請求內容(注意不是報文頭):

client_id=clientPC1   client_secret=123321   grant_type=client_credentials

  

把返回的 access_token 留下來後面用(注意有有效期)。  注意,其實不該該讓客戶端直接去申請 token,這只是咱演示,後面講解正確作法。

3、搭建 Ocelot 服務器項目

空 Web 項目,項目名 ID4.Ocelot1

nuget 安裝 IdentityServer四、Ocelot

編寫配置文件 Ocelot.json(注意設置【若是較新則】)

{
    "ReRoutes": [
        {
            "DownstreamPathTemplate": "/api/{url}",
            "DownstreamScheme": "http",
            "UpstreamPathTemplate": "/MsgService/{url}",
            "UpstreamHttpMethod": ["Get", "Post"],
            "ServiceName": "MsgService",
            "LoadBalancerOptions": {
                "Type": "RoundRobin"
            },
            "UseServiceDiscovery": true,
            "AuthenticationOptions": {
                "AuthenticationProviderKey": "MsgKey",
                "AllowedScopes": [] }
        },
        {
            "DownstreamPathTemplate": "/api/{url}",
            "DownstreamScheme": "http",
            "UpstreamPathTemplate": "/ProductService/{url}",
            "UpstreamHttpMethod": ["Get", "Post"],
            "ServiceName": "ProductService",
            "LoadBalancerOptions": {
                "Type": "RoundRobin"
            },
            "UseServiceDiscovery": true,
            "AuthenticationOptions": {
                "AuthenticationProviderKey": "ProductKey",
                "AllowedScopes": [] }
        }
    ],
        "GlobalConfiguration": {
        "ServiceDiscoveryProvider": {
            "Host": "localhost",
                "Port": 8500
        }
    }
}

 

 把/MsgService 訪問的都轉給消息後端服務器(使用Consul進行服務發現)。也能夠把Identity Server配置到Ocelot,可是咱們不作,後邊會講爲何不放。

Program.cs 的 CreateWebHostBuilder 中加載 Ocelot.json

.ConfigureAppConfiguration((hostingContext, builder) =>
{
         builder.AddJsonFile("Ocelot.json",false, true);
})

 修改 Startup.cs 讓 Ocelot 可以訪問 Identity Server 進行 Token 的驗證

using System;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
namespace ID4.Ocelot1
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //指定Identity Server的信息
            Action<IdentityServerAuthenticationOptions> isaOptMsg = o =>
            {
                o.Authority = "http://localhost:9500";
                o.ApiName = "MsgAPI";//要鏈接的應用的名字
                o.RequireHttpsMetadata = false;
                o.SupportedTokens = SupportedTokens.Both;
                o.ApiSecret = "123321";//祕鑰
 };
            Action<IdentityServerAuthenticationOptions> isaOptProduct = o =>
            {
                o.Authority = "http://localhost:9500";
                o.ApiName = "ProductAPI";//要鏈接的應用的名字
                o.RequireHttpsMetadata = false;
                o.SupportedTokens = SupportedTokens.Both;
                o.ApiSecret = "123321";//祕鑰            
 };
            services.AddAuthentication()
                //對配置文件中使用ChatKey配置了AuthenticationProviderKey=MsgKey
                //的路由規則使用以下的驗證方式
                .AddIdentityServerAuthentication("MsgKey", isaOptMsg).AddIdentityServerAuthentication("ProductKey", isaOptProduct); services.AddOcelot();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.         
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();

            }
            app.UseOcelot().Wait();
        }
    }
}

很顯然咱們可讓不一樣的服務採用不一樣的Identity Server。

啓動 Ocelot 服務器,而後向 ocelot 請求/MsgService/SMS/Send_MI(報文體仍是要傳 json 數據),在請求頭(不是報文體)里加上:

Authorization="Bearer "+上面 identityserver 返回的 accesstoken

 若是返回 401,那就是認證錯誤。

 Ocelot 會把 Authorization 值傳遞給後端服務器,這樣在後端服務器能夠用 IJwtDecoder 的這個不傳遞 key 的重載方法 IDictionary<string, object> DecodeToObject(string token),就能夠在不驗證的狀況下獲取 client_id 等信息。

 也能夠把 Identity Server 經過 Consul 進行服務治理。

 Ocelot+Identity Server 實現了接口的權限驗證,各個業務系統不須要再去作驗證。

4、不能讓客戶端請求 token

上面是讓客戶端去請求 token,若是項目中這麼搞的話,就把 client_id 特別是 secret 泄露給普通用戶的。

正確的作法應該是,開發一個 token 服務,由這個服務來向 identity Server 請求 token,客戶端向 token 服務發請求,把 client_id、secret 藏到這個 token 服務器上。固然這個服務器也要通過 Ocelot 轉發。

5、用戶名密碼登陸

若是 Api 和用戶名、密碼無關(好比系統內部之間 API 的調用),那麼上面那樣作就能夠了,可是有時候須要用戶身份驗證的(好比 Android 客戶端)。也就是在請求 token 的時候還要驗證用戶名密碼,在服務中還能夠獲取登陸用戶信息。

修改的地方:

一、 ID4.IdServer 項目中增長類 ProfileService.cs

using IdentityServer4.Models;
using IdentityServer4.Services;
using System.Linq;
using System.Threading.Tasks;
namespace ID4.IdServer
{
    public class ProfileService : IProfileService
    {
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var claims = context.Subject.Claims.ToList(); context.IssuedClaims = claims.ToList();
        }
        public async Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
        }
    }
}

 

增長類 ResourceOwnerPasswordValidator.cs

using IdentityServer4.Models;
using IdentityServer4.Validation;
using System.Security.Claims;
using System.Threading.Tasks;
namespace ID4.IdServer
{
    public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            //根據context.UserName和context.Password與數據庫的數據作校驗,判斷是否合法
            if (context.UserName == "yzk" && context.Password == "123")
            {
                context.Result = new GrantValidationResult(
          subject: context.UserName,
          authenticationMethod: "custom",
          claims: new Claim[] {
          new Claim("Name", context.UserName),
          new Claim("UserId", "111"),
          new Claim("RealName", "名字"),
          new Claim("Email", "qq@qq.com") }); } else { //驗證失敗 context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); } } } }

 固然這裏的用戶名密碼是寫死的,能夠在項目中鏈接本身的用戶數據庫進行驗證。claims 中能夠放入多組用戶的信息,這些信息均可以在業務系統中獲取到。

Config.cs

修改一下,主要是把GetClients中的AllowedGrantTypes屬性值改成GrantTypes.ResourceOwnerPassword,

而且在AllowedScopes中加入

IdentityServerConstants.StandardScopes.OpenId, //必需要添加,不然報forbidden錯誤                  

IdentityServerConstants.StandardScopes.Profile

修改後的 Config.cs

using System.Collections.Generic;
using IdentityServer4;
using IdentityServer4.Models;
namespace ID4.IdServer
{
    public class Config
    {
        /// <summary>
        /// 返回應用列表
        /// </summary>         
        /// <returns></returns>        
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> resources = new List<ApiResource>();
            //ApiResource第一個參數是應用的名字,第二個參數是描述
            resources.Add(new ApiResource("MsgAPI", "消息服務API"));
            resources.Add(new ApiResource("ProductAPI", "產品API"));
            return resources;
        }

        /// <summary>
        /// 返回客戶端帳號列表
        /// </summary>        
        /// <returns></returns>   
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>(); clients.Add(new Client
            {
                ClientId = "clientPC1",//API帳號、客戶端Id
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                ClientSecrets =
                {
                    new Secret("123321".Sha256())//祕鑰
                },
                AllowedScopes = { "MsgAPI","ProductAPI",IdentityServerConstants.StandardScopes.OpenId, //必需要添加,不然報forbidden錯誤
                     IdentityServerConstants.StandardScopes.Profile
                }//這個帳號支持訪問哪些應用
            }); return clients;
        }
    }
}

Startup.cs 的 ConfigureServices 修改成

public void ConfigureServices(IServiceCollection services)
{
    var idResources = new List<IdentityResource>
    {
            new IdentityResources.OpenId(), //必需要添加,不然報無效的 scope 錯誤               
            new IdentityResources.Profile()
    };
    services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryIdentityResources(idResources)
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())//
            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
            .AddProfileService<ProfileService>();
}

 主要是增長了 AddInMemoryIdentityResources 、 AddResourceOwnerValidator 、AddProfileService

二、 修改業務系統

以 MsgService 爲例

Nuget -> Install-Package IdentityServer4.AccessTokenValidation

而後 Startup.cs 的 ConfigureServices 中增長

services.AddAuthentication("Bearer")
   .AddIdentityServerAuthentication(options =>
   {
       options.Authority = "http://localhost:9500";//identity server 地址             
       options.RequireHttpsMetadata = false;
   });

Startup.cs 的 Configure 中增長

app.UseAuthentication();

三、 請求 token 把報文頭中的 grant_type 值改成 password,報文頭增長 username、password 爲用戶名、密碼。

 

像以前同樣用返回的 access_token傳遞給請求的Authorization 中,在業務系統的 User中就能夠獲取到 ResourceOwnerPasswordValidator 中爲用戶設置的 claims 等信息了。

public void Send_MI(dynamic model)
{
    string name = this.User.Identity.Name;//讀取的就是"Name"這個特殊的 Claims 的值
    string userId = this.User.FindFirst("UserId").Value; 
   string realName = this.User.FindFirst("RealName").Value;
   string email = this.User.FindFirst("Email").Value; Console.WriteLine($"name={name},userId={userId},realName={realName},email={email}"); Console.WriteLine($"經過小米短信接口向{model.phoneNum}發送短信{model.msg}"); }

四、 獨立登陸服務器解決上面提到的「不能讓客戶端接觸到 client_id、secret 的問題」

 開發一個服務應用 LoginService

public class RequestTokenParam
{
    public string username { get; set; }
    public string password { get; set; }
}
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace LoginService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        [HttpPost]
        public async Task<ActionResult> RequestToken(RequestTokenParam model)
        {
            Dictionary<string, string> dict = new Dictionary<string, string>();
            dict["client_id"] = "clientPC1";
            dict["client_secret"] = "123321";
            dict["grant_type"] = "password";
            dict["username"] = model.username;
            dict["password"] = model.password;
            //由登陸服務器向IdentityServer發請求獲取Token
            using (HttpClient http = new HttpClient()) 
    using (var content = new FormUrlEncodedContent(dict)) { var msg = await http.PostAsync("http://localhost:9500/connect/token", content);
          string result = await msg.Content.ReadAsStringAsync();
         return Content(result, "application/json"); } } } }

這樣客戶端只要向 LoginService 的 /api/Login/ 發請求帶上 json 報文體

{username:"yzk",password:"123"}便可。客戶端就不知道 client_secret 這些機密信息了。

 把 LoginService 配置到 Ocelot 中。

參考文章:https://www.cnblogs.com/jaycewu/p/7791102.html 

 

注:此文章是我看楊中科老師的.Net Core微服務第二版和.Net Core微服務第二版課件整理出來的

相關文章
相關標籤/搜索