微服务系列之授权认证(二) identity server 4
1.简介
IdentityServer4 是为ASP.NET Core系列量身打造的一款基于 OpenID Connect 和 OAuth 2.0 认证授权框架。
官方文档:https://identityserver4.readthedocs.io/en/latest/
框架源码:https://github.com/IdentityServer/IdentityServer4
IdentityServer主要使用场景:
1)基于中台认证中心的saas系统/pass系统的单点登录或者做为统一认证授权入口(授权模式:授权码模式Authorization Code或者混合模式hybrid);
2)用于API服务与API服务之间的固定token通讯,或者某业务系统服务群集与其他业务系统的服务群集之间通信,或者某业务系统群集服务与中台服务群集之间通信,所使用的授权模式为客户端模式Client Credential;
3)用于移动客户端与API服务之间通信,授权码模式为自定义授权码。
4)用于给第三方客户端授权使用平台数据资源,类似微信、支付宝等用户授权给。主要授权模式为权码模式Authorization Code
2.Identity Server入门demo
新建.net core 3.1项目,nuget安装IdentityServer4,我这里是3.14版本
正常来说,商业业务,Api资源、Client客户端、Identity资源、User等数据存储在数据库,token可以存储在数据库也可以存储到redis,这里为了入门演示,使用内存模式,快速搭建。
定义一个类,创建API资源,客户端client,我们这里只使用客户端模式授权,篇幅问题,其他授权方式就不一一写了,基本都差不多
public class TestConfig
{
/// <summary>
/// Api资源
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(){
Name = "myapi",
ApiSecrets= new List<Secret>(){
new Secret(){
Description = "secret",
Value = "secret".Sha256()
}
},
Scopes = new List<Scope>(){
new Scope(){
Name = "apim"
}
}
},
};
} /// <summary>
/// client
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{ return new List<Client>
{
new Client()
{
ClientId="client",//定义客户端ID //AllowedGrantTypes = new List<string>()
//{
// GrantTypes.ResourceOwnerPassword.FirstOrDefault(),
// GrantType.ClientCredentials,
// GrantType.Hybrid
//},
//必须是单个指定授权类型,可能是内存模式问题。
AllowedGrantTypes = GrantTypes.ClientCredentials,
// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes= {"apim"},
AccessTokenLifetime = 360000000
},
};
} public static List<TestUser> GetTestUsers()
{
return new List<TestUser>
{
new TestUser()
{
SubjectId = "1",
Username = "test",
Password = "123456"
}
};
}
}
在启动类注入
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
#region 内存方式
services.AddIdentityServer()
.AddDeveloperSigningCredential()//添加证书加密方式,执行该方法,会先判断tempkey.rsa证书文件是否存在,如果不存在的话,就创建一个新的tempkey.rsa证书文件,如果存在的话,就使用此证书文件。
.AddInMemoryApiResources(TestConfig.GetApiResources())//把受保护的Api资源添加到内存中
.AddInMemoryClients(TestConfig.GetClients())//客户端配置添加到内存中
.AddTestUsers(TestConfig.GetTestUsers())//测试的用户添加进来
.AddDeveloperSigningCredential();
#endregion
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
} app.UseStaticFiles(); app.UseRouting();
//添加中间件
//这个必须在UseRouting和UseEndpoints中间。如果IdentityServer服务端和API端要写在一起,
//那么这个必须在UseAuthorization和UseAuthentication的上面。
app.UseIdentityServer();
app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
然后启动服务,使用postman访问identity server 4默认的endpoint地址发现文档:
使用identityServer4的发现文档中的token_endpoint获取token
token已经获取了,可以使用发现文档里的introspection_endpoint来验证token
上图可见,我们已经为client客户端,创建了一个拥有访问scope为apim权限的token
接下来,创建一个受保护的api服务,同样创建一个.net core 3.1服务,并nuget包安装Microsoft.AspNetCore.Authentication.JwtBearer,选择3.14版本,根据.net core版本来
在启动类中,配置认证和授权DI,和添加认证授权中间件:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public void ConfigureServices(IServiceCollection services)
{
//配置认证
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:5000";//刚才启动的授权认证服务
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters //不验证jwt的aud信息
{
ValidateAudience = false
}; });
// 配置授权策略
services.AddAuthorization(options =>
{
//定义授权策略,这个名字可以随便起
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
//
policy.RequireClaim("scope", "apim");//策略需要scope有apim
});
options.AddPolicy("ApiScope2", policy =>
{
policy.RequireAuthenticatedUser();
//
policy.RequireClaim("scope", "apim2");
});
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder => builder.AllowAnyOrigin()
.WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS")
);
}).AddMvc();
services.AddControllers();
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Use((context, next) =>
{
context.Request.EnableBuffering();
return next();
});
app.UseRouting();
//跨域设置
app.UseCors(MyAllowSpecificOrigins); //身份验证中间件 (身份验证必须在授权的前面)
app.UseAuthentication(); //授权验证中间件
app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
写2个接口
注意,Authorize标签可生效于类或者方法上,,根据不同的授权策略来合理安排需要保护的资源。最后,可以用刚才的token来访问这个API,,如果token错误会401,如果根据授权策略的不同,比如mytwo接口受到apiScope2策略保护,apiScope2策略需要apim2这个scope权限,因为刚才我们获取的token只包含apim这个scope权限,所以访问会返回403权限不足,大家可以去试试,我试过了就不贴图。
至此demo结束,大家可以去试试其他模式的获取token方式
3.IdentityServer4的数据存储
商业级项目,授权资源是需要持久化存储的,官方已经提供了基于ef core的来维护我们授权资源和token的管理模型、上下文、仓储接口等,具体我就不写了,推荐参考这篇文章.net core 3.1 Identity Server4 (EntityFramework Core 配置) - 尘叶心繁的专栏 - TNBLOG。下面贴几张基于Identity Server4 EFCore包管理的APIResource、Client、Identity资源、Token的相关代码简介
services.AddIdentityServer()
.AddConfigurationStore(options => //注入idenity相关资源上下文
{
options.ResolveDbContextOptions = (provider, builder) =>
{
builder.UseSqlServer(Configuration.GetSection("Database:ConnectString").Value,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
.AddOperationalStore(options => //注入Token管理上下文
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetSection("Database:ConnectString").Value,
sql => sql.MigrationsAssembly(migrationsAssembly));
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 3600;
})
.AddDeveloperSigningCredential();
private ConfigurationDbContext _dbContext;
private PersistedGrantDbContext _grantdbContext; //这个就是identity资源上下文
private IOptions<IdentityOption> _identityOption; //这个就是token上下文
private IMediator _mediator;
public ClientManager(ConfigurationDbContext dbContext, IOptions<IdentityOption> identityOption, PersistedGrantDbContext grantdbContext, IMediator mediator)
{
_dbContext = dbContext;
_identityOption = identityOption;
_grantdbContext = grantdbContext;
_mediator = mediator;
}
下面是部分Client客户端管理代码
public async Task<Client> CreateClient(ClientEntity clientEntity)
{
if (_dbContext.Clients.Any(m => m.ClientName == clientEntity.ClientName))
throw new Exception("clientName Duplicate");
if (_dbContext.Clients.Any(m => m.ClientId == clientEntity.ClientId))
throw new Exception("clientId Duplicate");
IdentityServer4.EntityFramework.Entities.Client client = new IdentityServer4.EntityFramework.Entities.Client()
{
ClientId = clientEntity.ClientId,
ClientSecrets = new List<IdentityServer4.EntityFramework.Entities.ClientSecret>()
{
new IdentityServer4.EntityFramework.Entities.ClientSecret(){
Value=clientEntity.Sha256Secret,
Description=clientEntity.Secret
}
},
ClientName = clientEntity.ClientName,
// ClientUri = clientEntity.ClientUri,
Description = clientEntity.Description,
AccessTokenType = 1,
RequireConsent = clientEntity.RequireConsent,
AccessTokenLifetime = clientEntity.AccessTokenLifetime,
AllowOfflineAccess = true,
RedirectUris = new List<IdentityServer4.EntityFramework.Entities.ClientRedirectUri>(),
PostLogoutRedirectUris = new List<IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri>(),
AllowedGrantTypes = new List<IdentityServer4.EntityFramework.Entities.ClientGrantType>(),
Claims = new List<IdentityServer4.EntityFramework.Entities.ClientClaim>()
}; if (clientEntity.RedirectUris.Count > 0)
{
foreach (var url in clientEntity.RedirectUris)
{
client.RedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientRedirectUri()
{
RedirectUri = url
});
} }
if (clientEntity.PostLogoutRedirectUris.Count > 0)
{
foreach (var url in clientEntity.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri()
{
PostLogoutRedirectUri = url
});
} } //平台默认开放这三个类型
var typeList = new List<string>() { "hybrid", "client_credentials", "delegation" };
typeList.ForEach(type =>
{
client.AllowedGrantTypes.Add(new IdentityServer4.EntityFramework.Entities.ClientGrantType()
{
GrantType = type
});
}); var res = await _dbContext.Clients.AddAsync(client);
await _dbContext.SaveChangesAsync();
return res.Entity;
} public async Task<Client> UpdateClient(ClientEntity clientEntity)
{
if (_dbContext.Clients.Any(m => m.ClientName == clientEntity.ClientName && m.Id != clientEntity.Id))
throw new Exception("clientName Duplicate");
var client = await _dbContext.Clients
.Include(x => x.AllowedGrantTypes)
.Include(x => x.RedirectUris)
.Include(x => x.PostLogoutRedirectUris)
.Include(x => x.AllowedScopes)
.Include(x => x.ClientSecrets)
.Include(x => x.Claims)
.Include(x => x.IdentityProviderRestrictions)
.Include(x => x.AllowedCorsOrigins)
.Include(x => x.Properties)
.FirstOrDefaultAsync(x => x.Id == clientEntity.Id);
if (client == null)
throw new Exception("Client Not Exists!");
client.ClientName = clientEntity.ClientName;
client.Description = clientEntity.Description;
client.AccessTokenLifetime = clientEntity.AccessTokenLifetime;
client.RequireConsent = clientEntity.RequireConsent;
client.Enabled = clientEntity.Enabled;
client.RedirectUris = new List<IdentityServer4.EntityFramework.Entities.ClientRedirectUri>();
client.PostLogoutRedirectUris = new List<IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri>();
client.AllowedScopes = new List<IdentityServer4.EntityFramework.Entities.ClientScope>();
client.AllowedGrantTypes = new List<IdentityServer4.EntityFramework.Entities.ClientGrantType>(); if (clientEntity.RedirectUris.Count > 0)
{
foreach (var url in clientEntity.RedirectUris)
{
client.RedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientRedirectUri { RedirectUri = url });
}
} if (clientEntity.PostLogoutRedirectUris.Count > 0)
{
foreach (var url in clientEntity.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri { PostLogoutRedirectUri = url });
} } foreach (string scope in clientEntity.AllowedScopes)
{
client.AllowedScopes.Add(new IdentityServer4.EntityFramework.Entities.ClientScope { Scope = scope });
} foreach (string key in clientEntity.AllowedGrantTypes)
{
client.AllowedGrantTypes.Add(new IdentityServer4.EntityFramework.Entities.ClientGrantType { GrantType = key });
} var res = _dbContext.Clients.Update(client);
await _dbContext.SaveChangesAsync();
return res.Entity;
}
下面以部分token管理代码
public async Task<string> GenerateToken(int id, string nickName, string projectGroup, string contact, string useReason)
{
var client = await _dbContext.Clients
.Include(x => x.AllowedGrantTypes)
.Include(x => x.RedirectUris)
.Include(x => x.PostLogoutRedirectUris)
.Include(x => x.AllowedScopes)
.Include(x => x.ClientSecrets)
.Include(x => x.Claims)
.Include(x => x.IdentityProviderRestrictions)
.Include(x => x.AllowedCorsOrigins)
.Include(x => x.Properties)
.FirstOrDefaultAsync(x => x.Id == id);
if (client == null)
throw new Exception("Client Not Exists!");
//初始化连接IdentityServer客户端,这也是人家封装好的http请求
var discoveryClient = new DiscoveryClient(_identityOption.Value.Host)
{
Policy = new DiscoveryPolicy { RequireHttps = _identityOption.Value.Https, ValidateIssuerName = false }
};
//获取endpint
var discoveryResponse = await discoveryClient.GetAsync();
//连接获取token那个endpoint
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint,
client.ClientId,
client.ClientSecrets.FirstOrDefault().Description);
#region 计算当前client的拥有的API资源SCOPE
var allScopes = client.AllowedScopes.Select(p => p.Scope).ToList();
var apiResourceScopes = new List<string>();
_dbContext.ApiResources.Include("Scopes").ToList().ForEach(api =>
{
if (api.Scopes.Count > 0)
apiResourceScopes.AddRange(api.Scopes.Select(p => p.Name).ToList());
});
var inScopes = apiResourceScopes.Intersect(allScopes);
#endregion
var scope = string.Join(" ", inScopes);
//请求生成客户端模式的token
var tokenResponse = await tokenClient.RequestCustomGrantAsync("client_credentials", scope); if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
else
{
//发送事件
var _key = string.Format("{0}:reference_token", tokenResponse.AccessToken).ToSha256();
var tokenEntity = new TokenEntity(_key, nickName, projectGroup, contact, useReason, client.ClientId, tokenResponse.AccessToken);
await _mediator.Publish<GenerateTokenEvent>(new GenerateTokenEvent(tokenEntity));
return tokenResponse.AccessToken;
}
}
下面还有一段关于修改token过期时间的代码
public async Task<bool> AddExpiration(string token, DateTime date)
{
if (!_dbContext.TokenEntities.Any(p => p.Token == token))
throw new Exception("manage token not exsits");
//这个key是经过一定格式后进行sha256加密后,作为数据库表PersistedGrants一个唯一标识
var _key = string.Format("{0}:reference_token", token).ToSha256();
var persistedGrant = _grantdbContext.PersistedGrants.FirstOrDefault(p => p.Key == _key);
if (persistedGrant == null)
throw new Exception("token不存在或者TOKEN已过期");
var data = JObject.Parse(persistedGrant.Data);
var creation = data.Value<DateTime>("CreationTime");
var lifetime = data.Value<int>("Lifetime"); data["Lifetime"] = (int)((date - creation).TotalSeconds); persistedGrant.Expiration = date;
persistedGrant.Data = data.ToString(Newtonsoft.Json.Formatting.None); _grantdbContext.PersistedGrants.Update(persistedGrant);
await _grantdbContext.SaveChangesAsync();
return true;
}
再来一段自定义授权模式代码
public class DelegationGrantValidator : IExtensionGrantValidator//需要继承一下类型验证扩展接口
{
private readonly ITokenValidator _validator;//identity框架已实现的token验证服务,直接注入使用 public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
} public string GrantType => "delegation";//自定义的授权类型,我这实现的是一个token交换token的类型 public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");//获取被交换token if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
} var result = await _validator.ValidateAccessTokenAsync(userToken);//直接使用人家实现好的token验证服务验证传来的token
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
} //声明获取用户,如果有用户,说明要换取用户token
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (sub != null)
{
context.Result = new GrantValidationResult(sub, GrantType);//换取用户token,
return;
} // 声明中获取客户端ID,如果有clientId,说明换取客户端token
var client_id = result.Claims.FirstOrDefault(c => c.Type == "client_id")?.Value;
if (client_id != null)
{
//context.Result = new GrantValidationResult(client_id, GrantType);
context.Result = new GrantValidationResult();//换取客户端token
return;
} context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);
}
} builder.AddExtensionGrantValidator<DelegationGrantValidator>();注入DI。
IdentityServer4还可以扩展endpoint,但是扩展完后,在发现文档不显示,但是可以作为http使用,以下代码截图供参考
4.结尾
identityServer4要写的东西实在太多,整体的把握理解还是有一定的复杂性的,我之前公司一个pass平台项目,是基于认证中心,其他业务系统实现快速集成,我当时负责的就是授权资源、token管理,还有对IDP的授权类型、endpoint一些扩展,现在总结成博客,写的不是很细,希望对后来者带来一些帮助和参考意义。
微服务系列之授权认证(二) identity server 4的更多相关文章
- 微服务系列之授权认证(一) OAuth 2.0 和 OpenID Connect
1.传统架构的授权认证 传统应用架构,用户使用账号密码登录后,可以使用前端cookie存储登录状态,也可以使用后端session方式存储登录状态,小应用这么做其实很高效实用,当应用需要横向扩展时,就需 ...
- 微服务系列之授权认证(三) JWT
1.JWT简介 官方定义:JWT是JSON Web Token的缩写,JSON Web Token是一个开放标准(RFC 7519),它定义了一种紧凑的.自包含的方式,可以将各方之间的信息作为JSON ...
- 【微服务】之二:从零开始,轻松搞定SpringCloud微服务系列--注册中心(一)
微服务体系,有效解决项目庞大.互相依赖的问题.目前SpringCloud体系有强大的一整套针对微服务的解决方案.本文中,重点对微服务体系中的服务发现注册中心进行详细说明.本篇中的注册中心,采用Netf ...
- 微服务系列(二):使用 API 网关构建微服务
编者的话|本文来自 Nginx 官方博客,是微服务系列文章的第二篇,本文将探讨:微服务架构是如何影响客户端到服务端的通信,并提出一种使用 API 网关的方法. 作者介绍:Chris Richardso ...
- 微服务系列(二)GRPC的介绍与安装
微服务系列(二)GRPC的介绍与安装 1.GPRC简介 GRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架.GRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多 ...
- 【转】「Chris Richardson 微服务系列」微服务架构的优势与不足
Posted on 2016年5月4日 编者的话|本文来自 Nginx 官方博客,是微服务系列文章的第一篇,主要探讨了传统的单体式应用的不足,以及微服务架构的优势与挑战. 作者介绍:Chris Ric ...
- 从零开始,轻松搞定SpringCloud微服务系列
本系列博文目录 [微服务]之一:从零开始,轻松搞定SpringCloud微服务系列–开山篇(spring boot 小demo) [微服务]之二:从零开始,轻松搞定SpringCloud微服务系列–注 ...
- Dubbo 微服务系列(03)服务注册
Dubbo 微服务系列(03)服务注册 [TOC] Spring Cloud Alibaba 系列目录 - Dubbo 篇 1. 背景介绍 图1 Dubbo经典架构图 注:本图来源 Dubbo官方架构 ...
- Spring Cloud微服务系列文,服务调用框架Feign
之前博文的案例中,我们是通过RestTemplate来调用服务,而Feign框架则在此基础上做了一层封装,比如,可以通过注解等方式来绑定参数,或者以声明的方式来指定请求返回类型是JSON. 这种 ...
随机推荐
- 从入门到爱上Git
时间不在于你拥有多少,而在于你怎样使用------时之沙 · 艾克 一.Git设置 1.1 Git全局设置 当我们安装好Git以后,我们需要对Git进行账号.邮箱的设置 设置用户信息 git conf ...
- python sock5代理
安装 pysocks:pip install pysocks # coding:utf-8 ''' @version: python3.6 @author: 'eric' @license: Apac ...
- vue 调用nginx服务跨越的问题
server { listen 80; server_name api.xxxx.com; add_header Access-Control-Allow-Origin '*' ; proxy_set ...
- NOI / 2.1基本算法之枚举-8760:Cantor表
总时间限制: 1000ms 内存限制: 65536kB 描述 现代数学的著名证明之一是Georg Cantor证明了有理数是可枚举的.他是用下面这一张表来证明这一命题的: 我们以Z字形给上表的每一项编 ...
- SQL语句的整合
基础语法 https://blog.csdn.net/m0_37989980/article/details/103413942 CRUD 提供给数据库管理员的基本操作,CRUD(Create, Re ...
- 抖音 滑块验证方案 s_v_web_id 参数分析
本文所有教程及源码.软件仅为技术研究.不涉及计算机信息系统功能的删除.修改.增加.干扰,更不会影响计算机信息系统的正常运行.不得将代码用于非法用途,如侵立删! 抖音web端 s_v_web_id 参数 ...
- 个人学习-STL深入学习01-vectory源码研习 // 需要补充
参考资料: [1]博主:一枚程序员 STL源码剖析--vector https://www.cnblogs.com/sooner/p/3273395.html [2]博主:劲蜡鸡腿堡 vector源码 ...
- LuoguP5201 [USACO19JAN]Shortcut(最短路树)
字典序?建树时从小枚举,用\(Dijkstra\)的血泪建好树,\(size\)大小决定贡献 #include <iostream> #include <cstdio> #in ...
- java-前端之css
css样式: <!-- 内联样式:在元素的style属性内写样式 --> <h2 style="color: red;">愿你单枪匹马,亦能所向披靡!< ...
- Excel 笔记目录
前言 Excel 是微软(Microsoft)公司推出的 Office 办公系列软件的一个重要组成部分,主要用于电子表格处理,可以高效地完成各种表格和图表的设计,进行复杂的数据计算和分析. 一句科普 ...