我的微服务项目之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的文件用来配置一些认证资源
1 2 3 4 5 6 7 8 9 10 11 12 13 | { "WebApi" : { "ApiResource" : "" ,<br> "ApiScope" : "" , "Client" : { "ClientId" : "" , "ClientName" : "" , "ClientSecrets" : "" , "AllowedGrantTypes" : "" , "AllowedScopes" : "" , "AccseeTokenTime" : "" //单位秒 } } } |
然后添加ApiConfig类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | 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版本之后都要这样写
1 2 3 | ApiResource apiResource = new ApiResource(name, name) { Scopes = scopes }; |
然后开始添加服务认
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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,只需要简单的配置,如下部分截图:
然后添加配置节点:
1 2 3 4 5 6 7 | "IdentityService" : { "Uri" : "http://localhost:5003" ,//自己的认证服务地址,网关的地址为5000 "UseHttps" : false , "ApiName" : { "WebApi" : "WebApi" } } |
并且在Startup里面添加如下代码:
1 2 3 4 5 6 7 8 9 | #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的方式,这样就可以了。如下:
1 2 3 4 5 6 | location /auth/credentials/token { proxy_set_header client_secret "" ; proxy_set_header client_id "" ; proxy_pass http: //127.0.0.1:5000; } //我这里是通过Ocelot转发到认证服务的,不要盲目复制 |
然后认证服务新加一个api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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,如果有不合理或错误的地方希望大家指出,欢迎大家访问我的站点:天天博客
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库