使用 IdentityServer4 实现 OAuth 2.0 与 OpenID Connect 服务
IdentityServer4 是 ASP.NET Core 的一个包含 OIDC 和 OAuth 2.0 协议的框架。最近的关注点在 ABP 上,默认 ABP 也集成 IdentityServer4,之前也介绍了很多 IdentityServer3 相关的文章(IdentityServer3 已停止维护)。今天简单记录一下 IdentityServer4 相关配置。
IdentityServer实现以下规范:
OpenID Connect
- OpenID Connect Core 1.0 (spec)
- OpenID Connect Discovery 1.0 (spec)
- OpenID Connect Session Management 1.0 - draft 22 (spec)
- OpenID Connect HTTP-based Logout 1.0 - draft 03 (spec)
OAuth 2.0
- OAuth 2.0 (RC-6749)
- OAuth 2.0 Bearer Token Usage (RFC 6750)
- OAuth 2.0 Multiple Response Types (spec)
- OAuth 2.0 Form Post Response Mode (spec)
- OAuth 2.0 Token Revocation (RFC 7009)
- OAuth 2.0 Token Introspection (RFC 7662)
- Proof Key for Code Exchange (RFC 7636)
- JSON Web Tokens for Client Authentication (RFC 7523)
OpenID Connect (OIDC)
OIDC 是 OpenID Connect 的简称,OIDC=(Identity, Authentication) + OAuth 2.0,OpenID Connect 在 OAuth2(兼容) 上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。我们都知道OAuth2是一个授权协议,它无法提供完善的身份认证功能,OpenID Connect 使用 OAuth2 的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用),且完全兼容OAuth2,也就是说你搭建了一个OpenID Connect 的服务后,也可以当作一个OAuth2的服务来用。OAuth2 提供了 Access Token 来解决授权第三方客户端访问受保护资源的问题;OIDC 在这个基础上提供了 ID Token 来解决第三方客户端标识用户身份认证的问题。ID Token 使用 JWT(JSON Web Token) 格式来包装防篡改,使得 ID Token 可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了 UserInfo 的接口,以便获取用户更完整的信息。
协议规范
- Core – Defines the core OpenID Connect functionality: authentication built on top of OAuth 2.0 and the use of Claims to communicate information about the End-User
- Discovery – (Optional) Defines how Clients dynamically discover information about OpenID Providers
- Dynamic Registration – (Optional) Defines how clients dynamically register with OpenID Providers
- OAuth 2.0 Multiple Response Types – Defines several specific new OAuth 2.0 response types
- OAuth 2.0 Form Post Response Mode – (Optional) Defines how to return OAuth 2.0 Authorization Response parameters (including OpenID Connect Authentication Response parameters) using HTML form values that are auto-submitted by the User Agent using HTTP POST
- Session Management – (Optional) Defines how to manage OpenID Connect sessions, including postMessage-based logout functionality
- Front-Channel Logout – (Optional) Defines a front-channel logout mechanism that does not use an OP iframe on RP pages
- Back-Channel Logout – (Optional) Defines a logout mechanism that uses direct back-channel communication between the OP and RPs being logged out
流程规范
+--------+ +--------+
| | | |
| |---------() AuthN Request-------->| |
| | | |
| | +--------+ | |
| | | | | |
| | | End- |<--() AuthN & AuthZ-->| |
| | | User | | |
| RP | | | | OP |
| | +--------+ | |
| | | |
| |<--------() AuthN Response--------| |
| | | |
| |---------() UserInfo Request----->| |
| | | |
| |<--------() UserInfo Response-----| |
| | | |
+--------+ +--------+
GET /authorize?
response_type=code%20id_token%20token
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&scope=openid%20profile%20email
&nonce=n-0S6_WzA2Mj
&state=af0ifjsldkj HTTP/1.1
Host: server.example.com HTTP/1.1 Found
Location: https://client.example.org/cb#
code=Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk
&access_token=jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y
&token_type=Bearer
&id_token=eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogIml
zcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ
4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiA
ibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDE
zMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEE
iCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcN
egx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp
_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWh
sPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL
7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_
gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ
OIDC 的流程由五个步骤构成:
- RP发送一个认证请求给OP;
- OP对EU进行身份认证,然后提供授权;
- OP把ID Token和Access Token(需要的话)返回给RP;
- RP使用Access Token发送一个请求UserInfo EndPoint;
- UserInfo EndPoint返回EU的Claims。
OpenID Connect Flows
虽然在 OIDC中 是复用 OAuth2 的流程,但是用途是不一样的,且 OIDC 的 AuthN 请求中 scope 参数必须要有一个值为 openid 的参数。另外定义的模式规范也不一样,具体如下:
ID Token
id_token 是一个安全令牌,是一个授权服务器提供的包含用户信息(由一组Cliams构成以及其他辅助的Cliams)的 JWT 格式的数据结构。一般实际生产环境可能还需要我们自己扩展 Claims(默认 OIDC 提供了一组公共的 Cliams),一般使用 JWK 签名,验签需要 RSA public key (IdentityServer 中从 /.well-known/openid-configuration/jwks 获得 )。
对应 IdentityServer 中配置
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResources.Address()
};
}
...
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
配置 Idsr4 服务端
Idr4.HostSrv
/// <summary>
/// 配置信息
/// </summary>
public class InMemoryConfig
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
//new IdentityResources.Email(),
//new IdentityResources.Phone(),
//new IdentityResources.Address()
};
} /// <summary>
/// Define which APIs will use this IdentityServer
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource("api", "api service")
{
ApiSecrets = { new Secret("api_pwd".Sha256()) }
},
new ApiResource("user", "user service"),
new ApiResource("order", "order service")
};
} /// <summary>
/// Define which Apps will use thie IdentityServer
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{
return new[]
{
// client credentials client
new Client
{
ClientId = "client_credentials_jwt_grant",
ClientSecrets = new [] { new Secret("".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenType=AccessTokenType.Jwt,
AllowedScopes = GetApiResources().Select(t=>t.Name).ToArray()
},
// client credentials client
new Client
{
ClientId = "client_credentials_reference_grant",
ClientSecrets = new [] { new Secret("".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenType=AccessTokenType.Reference,
AllowedScopes = GetApiResources().Select(t=>t.Name).ToArray()
},
// resource owner password grant client
new Client
{
ClientId = "client_password_grant",
ClientSecrets = new [] { new Secret("".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AccessTokenType=AccessTokenType.Reference,
AllowedScopes = new [] { "user", "order"},
},
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
// Gets or sets a value indicating whether [allow offline access scope]. Defaults to false.
AllowOfflineAccess = true
}, // Implicit grant client
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:5003/callback.html" },
PostLogoutRedirectUris = { "http://localhost:5003/index.html" },
AllowedCorsOrigins = { "http://localhost:5003" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
}
};
} /// <summary>
/// Define which uses will use this IdentityServer
/// </summary>
/// <returns></returns>
public static IEnumerable<TestUser> GetUsers()
{
return new[]
{
new TestUser
{
SubjectId = "",
Username = "irving",
Password = "",
Claims = new List<Claim>
{
new Claim("name", "irving"),
new Claim("website", "https://irving.com")
}
},
new TestUser
{
SubjectId = "",
Username = "ytzhou",
Password = "",
Claims = new List<Claim>
{
new Claim("name", "ytzhou"),
new Claim("website", "https://ytzhou.com")
}
}
};
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
}); // configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseSuccessEvents = true;
})
//.AddDeveloperSigningCredential()
//.AddDeveloperSigningCredential(persistKey: false)
//.AddDeveloperSigningCredential(persistKey: true, filename: "rsakey.rsa")
.AddSigningCredential(new X509Certificate2(Path.Combine(AppContext.BaseDirectory, Configuration["Certs:Path"]), Configuration["Certs:Pwd"]))
.AddInMemoryApiResources(InMemoryConfig.GetApiResources())
.AddInMemoryIdentityResources(InMemoryConfig.GetIdentityResources())
.AddInMemoryClients(InMemoryConfig.GetClients())
.AddTestUsers(InMemoryConfig.GetUsers().ToList());
// .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
//.AddProfileService<ProfileService>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
} // 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseIdentityServer(); // pipeline with a default route named 'default' and the following template: '{controller=Home}/{action=Index}/{id?}'.
//app.UseMvcWithDefaultRoute();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Client
static void Main(string[] args)
{
Task.Run(() =>
{
return Run();
});
Console.ReadLine();
} public static async Task Run()
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
//jwt
var client = new TokenClient(disco.TokenEndpoint, "client_1", "");
var response = await client.RequestClientCredentialsAsync("api");
if (response.IsError)
{
Console.WriteLine(response.Error);
return;
}
Console.WriteLine(response.Json);
}
}
报文
GET http://localhost:5000/.well-known/openid-configuration HTTP/1.1
Host: localhost: HTTP/1.1 OK
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XC53ZWxsLWtub3duXG9wZW5pZC1jb25maWd1cmF0aW9u?=
X-Powered-By: ASP.NET
Date: Tue, Jul :: GMT
Content-Length: {"issuer":"http://localhost:5000","jwks_uri":"http://localhost:5000/.well-known/openid-configuration/jwks","authorization_endpoint":"http://localhost:5000/connect/authorize","token_endpoint":"http://localhost:5000/connect/token","userinfo_endpoint":"http://localhost:5000/connect/userinfo","end_session_endpoint":"http://localhost:5000/connect/endsession","check_session_iframe":"http://localhost:5000/connect/checksession","revocation_endpoint":"http://localhost:5000/connect/revocation","introspection_endpoint":"http://localhost:5000/connect/introspect","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["api","user","order","offline_access"],"claims_supported":[],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","password"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"code_challenge_methods_supported":["plain","S256"]} GET http://localhost:5000/.well-known/openid-configuration/jwks HTTP/1.1
Host: localhost:
HTTP/1.1 OK
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XC53ZWxsLWtub3duXG9wZW5pZC1jb25maWd1cmF0aW9uXGp3a3M=?=
X-Powered-By: ASP.NET
Date: Tue, Jul :: GMT
Content-Length: {"keys":[{"kty":"RSA","use":"sig","kid":"0fdf841efb8c990ea6f2b09318c0cba2","e":"AQAB","n":"zDMobgJ8pjUAH_e8EqtYZE-t14InmDDcpDqdQp9bT0bGiOpvLpgqgsFJulAwKQfhPwwOwUBKq7Lle461Gb1PRug4L1zN3U-WA9cj0LL4dAHqGCXEazl3FTvWGe8FrQQRTgi8q-I2X_Jhxp8BYQkfatFknVUZSDYudxL-fIDJOSVYus-oEfhupQf_b1Le27UvfMuswVsUhKHbL2wSy_ZtdbY1X8pJ5XoLJwL2AO62Ahfb8ptHBI_Nbc285hAuB4WTPVcIdpp99Oodf6wTiflTVWLGqWP3o48VlxNyixUJCWqWI78BTno06U9cISBTAwbXFLADqjJDYz4OZOAn7Np_DQ","alg":"RS256"}]} POST http://localhost:5000/connect/token HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length:
Host: localhost: grant_type=client_credentials&scope=api HTTP/1.1 OK
Cache-Control: no-store, no-cache, max-age=
Pragma: no-cache
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcdG9rZW4=?=
X-Powered-By: ASP.NET
Date: Tue, Jul :: GMT
Content-Length: {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjBmZGY4NDFlZmI4Yzk5MGVhNmYyYjA5MzE4YzBjYmEyIiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MzI0MzYzMjIsImV4cCI6MTUzMjQzOTkyMiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkiXSwiY2xpZW50X2lkIjoiY2xpZW50XzEiLCJzY29wZSI6WyJhcGkiXX0.xEucJ6N8HQENfB17dFJlWsGzwI8jdeazLlYdUHcd4OW1BxMjT3CRA0HAkRQl4RHz3GGN9cDjZrg-_mx80Vtk_25sd8eopkuvQAU8OgoHs7c9br0yfnF0SgYglKg-ua5cEPX14EylSDydWltSSERvsOB-4jQBE1N9ZN_ukKzwgKwGtE_k5lHShlhr_00HW3bT-u25SJtl5rAkpoQxPFAo6IVRwm8Wu-pQtLb4tNRn9iavOEehDFT-PlU6ZBlzxAnznN4ul5MLYyRmK5V8tg07ZPyKYPOPk9bFEOhBQtXTyV0KdMtNYpr4KXFgxoMPpSaFZYwQPbCJJm4YmKhCgAdAJg","expires_in":,"token_type":"Bearer"} POST http://localhost:5000/connect/token HTTP/1.1
Authorization: Basic Y2xpZW50XzI6MTIzNDU2
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length:
Host: localhost: grant_type=client_credentials&scope=order HTTP/1.1 OK
Cache-Control: no-store, no-cache, max-age=
Pragma: no-cache
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcdG9rZW4=?=
X-Powered-By: ASP.NET
Date: Tue, Jul :: GMT
Content-Length: {"access_token":"10f821709ed30dde85e3c1de73a5cb941bf85f77703e544f8cd779d5751ba7c5","expires_in":,"token_type":"Bearer"}
保护资源服务API 配置
上面我们定义了 ApiResource 配置
/// <summary>
/// Define which APIs will use this IdentityServer
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource("api", "api service")
{
ApiSecrets = { new Secret("api_pwd".Sha256()) }
},
new ApiResource("user", "user service"),
new ApiResource("order", "order service")
};
}
可以访问 /.well-known/openid-configuration 查看到有哪些 scopes 配置。
[
"openid",
"profile",
"api",
"user",
"order",
"offline_access"
]
其中关于 ApiSecrets 的配置只配置 api 的 scope ,这个配置只适用于后续资源端去验证 token 是否合法性用到 (调用 introspection endpoint ),只适用于Token 是 Reference token 的方式, Self-contained Json Web Token 采用公钥的方式验签,也就是没有定义 ApiSecrets 的 scope (如 user,order ) 是不能使用 Reference token 的 Token 的。
资源 API 端
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//jwt
//services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
// .AddIdentityServerAuthentication(options =>
// {
// options.Authority = "http://localhost:5000";
// options.RequireHttpsMetadata = false;
// options.ApiName = "api";
// }); //Enable reference tokens
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api";
options.ApiSecret = "api_pwd";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
} // 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.UseAuthentication();
app.UseMvc();
}
}
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
} // GET api/values/1
[HttpGet("{id}")]
[Authorize]
public ActionResult<IEnumerable<string>> Get(int id)
{
try
{
return new JsonResult(from c in HttpContext.User.Claims select new { c.Type, c.Value });
}
catch (Exception ex)
{
return new string[] { ex.Message };
}
}
}
客户端(仅测试 client credentials grant)
client_credentials_jwt_grant
public static async Task Run()
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
var client = new TokenClient(disco.TokenEndpoint, "client_credentials_jwt_grant", "");
var response = await client.RequestClientCredentialsAsync("api");
if (response.IsError)
{
Console.WriteLine(response.Error);
Console.Read();
}
Console.WriteLine(response.Json);
//call api
var http = new HttpClient();
http.SetBearerToken(response.AccessToken);
var message = await http.GetAsync("http://localhost:17181/api/values/1");
if (!message.IsSuccessStatusCode)
{
Console.WriteLine(message.ReasonPhrase);
Console.Read();
}
Console.WriteLine(message.Content.ReadAsStringAsync().Result);
}
上述 AccessTokenType 定义为 Jwt,调用 scope 为 api 接口产生的报文如下
POST http://localhost:5000/connect/token HTTP/1.1
Authorization: Basic Y2xpZW50X2NyZWRlbnRpYWxzX2p3dF9ncmFudDoxMjM0NTY=
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length:
Host: localhost: grant_type=client_credentials&scope=api HTTP/1.1 OK
Cache-Control: no-store, no-cache, max-age=
Pragma: no-cache
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcdG9rZW4=?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IkI0RjdDNTUzM0EwNkIyMkU2RDM0OUJFRkQ4NEI3NkU3MzAxNjFCNTUiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJ0UGZGVXpvR3NpNXROSnZ2MkV0MjV6QVdHMVUifQ.eyJuYmYiOjE1MzI1MTg1NTMsImV4cCI6MTUzMjUyMjE1MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkiXSwiY2xpZW50X2lkIjoiY2xpZW50X2NyZWRlbnRpYWxzX2p3dF9ncmFudCIsInNjb3BlIjpbImFwaSJdfQ.rI6oO_L9__lhO6twm8IFfnTDFLHk0zg7rcyue1IC9i85rkgVDeN4Jda4AVNIh4mnY4v7mQT4qizsvDxD7UbSyBkfr9Sq3qjeIHFUsmBzihcSKI2yzOerNCoCm9mdTG7EYgYFWDN_XvD92PTcYh8LGM4rGnCWlozlNU2u2sDHQ55yWvSwAzoGU2xk7vkYhlc1cqYl8gzHfX3lX4iCLPb7uFHOwYX3-ymsBHhVm4rYas9Xf9sefJaJj0Z3HVgp9dhkDxCtU-k5k519xB7MR9HozJSlM74j2z7kAdR8apX2yIeNa-YTzjsqHcoCi1dblc9jQZE05fmFtAdmbf637CrpFw","expires_in":,"token_type":"Bearer"} GET http://localhost:17181/api/values/1 HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkI0RjdDNTUzM0EwNkIyMkU2RDM0OUJFRkQ4NEI3NkU3MzAxNjFCNTUiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJ0UGZGVXpvR3NpNXROSnZ2MkV0MjV6QVdHMVUifQ.eyJuYmYiOjE1MzI1MTg1NTMsImV4cCI6MTUzMjUyMjE1MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkiXSwiY2xpZW50X2lkIjoiY2xpZW50X2NyZWRlbnRpYWxzX2p3dF9ncmFudCIsInNjb3BlIjpbImFwaSJdfQ.rI6oO_L9__lhO6twm8IFfnTDFLHk0zg7rcyue1IC9i85rkgVDeN4Jda4AVNIh4mnY4v7mQT4qizsvDxD7UbSyBkfr9Sq3qjeIHFUsmBzihcSKI2yzOerNCoCm9mdTG7EYgYFWDN_XvD92PTcYh8LGM4rGnCWlozlNU2u2sDHQ55yWvSwAzoGU2xk7vkYhlc1cqYl8gzHfX3lX4iCLPb7uFHOwYX3-ymsBHhVm4rYas9Xf9sefJaJj0Z3HVgp9dhkDxCtU-k5k519xB7MR9HozJSlM74j2z7kAdR8apX2yIeNa-YTzjsqHcoCi1dblc9jQZE05fmFtAdmbf637CrpFw
Host: localhost: HTTP/1.1 OK
Content-Type: application/json; charset=utf-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5XZWJBcGlcYXBpXHZhbHVlc1wx?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: [{"type":"nbf","value":""},{"type":"exp","value":""},{"type":"iss","value":"http://localhost:5000"},{"type":"aud","value":"http://localhost:5000/resources"},{"type":"aud","value":"api"},{"type":"client_id","value":"client_credentials_jwt_grant"},{"type":"scope","value":"api"}] GET http://localhost:5000/.well-known/openid-configuration/jwks HTTP/1.1
Host: localhost:
HTTP/1.1 OK
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XC53ZWxsLWtub3duXG9wZW5pZC1jb25maWd1cmF0aW9uXGp3a3M=?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: {"keys":[{"kty":"RSA","use":"sig","kid":"B4F7C5533A06B22E6D349BEFD84B76E730161B55","x5t":"tPfFUzoGsi5tNJvv2Et25zAWG1U","e":"AQAB","n":"zDXSeNo4oO-Tn372eKUywF40D0HG4XXeYtbYtdnpVsIZkDDouZr2jFeq3C-AUb546CJXFqqZj6YZPOMtiHBfzyDGThd45mQvNwQ18B7lae4vab1hvxx9HZGku64Wy5JlqT2jHJ-WR7GS9OZjHSeioMoDE654LhDxJthfj_C2G0jA_RTnPQKnQgciv5JiENTUwrghr9cXzBNgPE0QLAhKrCEoVoSxYOWTL9EBCUc2DB2Vah7RHNfNItrXbrdqvrDQ5rXBH8Rq6irjSF_FjcuIwMkTmLOkswnC_qBN7qjbmgLRIxG3YiSnZR5bgyhjFWNzea0jmuWEiFIIIMwTfPXpPw","x5c":["MIID8TCCAtmgAwIBAgIJAIRTKytMROvuMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTAeFw0xODA3MjUwNzI3MzZaFw0xOTA3MjUwNzI3MzZaMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMw10njaOKDvk59+9nilMsBeNA9BxuF13mLW2LXZ6VbCGZAw6Lma9oxXqtwvgFG+eOgiVxaqmY+mGTzjLYhwX88gxk4XeOZkLzcENfAe5WnuL2m9Yb8cfR2RpLuuFsuSZak9oxyflkexkvTmYx0noqDKAxOueC4Q8SbYX4/wthtIwP0U5z0Cp0IHIr+SYhDU1MK4Ia/XF8wTYDxNECwISqwhKFaEsWDlky/RAQlHNgwdlWoe0RzXzSLa1263ar6w0Oa1wR/Eauoq40hfxY3LiMDJE5izpLMJwv6gTe6o25oC0SMRt2Ikp2UeW4MoYxVjc3mtI5rlhIhSCCDME3z16T8CAwEAAaNQME4wHQYDVR0OBBYEFOK5Y2P7/L8KsOrPB+glPVkKi2VOMB8GA1UdIwQYMBaAFOK5Y2P7/L8KsOrPB+glPVkKi2VOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEnXXws/cBx5tA9cBfmkqGWzOU5/YmH9pzWchJ0ssggIqZVx0yd6ok7+C+2vKIRMp5E6GCfXWTB+LI7qjAVEvin1NwGZ06yNEsaYaJYMC/P/0TunoMEZmsLM3rk0aISbzkNciF+LVT16i0C+hT1+Pyr8lP4Ea1Uw0n50Np6SOwQ6e2PMFFOIaqjG94tuCN3RX819IJSQPbq9FtRmNvmbWPM1v2CO6SYT51SvsIHnZyn0rAK+h/hywVQqmI5ngi1nErIQEqybkZj00OhmYpAqsetWYU5Cs1qhJ70kktlrd+jMHdarVB9ko0h+ij6HL22mmBYAb7zVGWyDroNJVhEw6DA="],"alg":"RS256"}]}
client_credentials_reference_grant
static void Main(string[] args)
{
Task.Run(() =>
{
return Run();
});
Console.ReadLine();
} public static async Task Run()
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
var client = new TokenClient(disco.TokenEndpoint, "client_credentials_reference_grant", "");
var response = await client.RequestClientCredentialsAsync("api");
if (response.IsError)
{
Console.WriteLine(response.Error);
Console.Read();
}
Console.WriteLine(response.Json);
//call api
var http = new HttpClient();
http.SetBearerToken(response.AccessToken);
var message = await http.GetAsync("http://localhost:17181/api/values/1");
if (!message.IsSuccessStatusCode)
{
Console.WriteLine(message.ReasonPhrase);
Console.Read();
}
Console.WriteLine(message.Content.ReadAsStringAsync().Result);
}
上述 AccessTokenType 定义为 Reference ,调用 scope 为 api 接口产生的报文如下:
获得 Token 的报文
POST http://localhost:5000/connect/token HTTP/1.1
Authorization: Basic Y2xpZW50X2NyZWRlbnRpYWxzX3JlZmVyZW5jZV9ncmFudDoxMjM0NTY=
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length:
Host: localhost: grant_type=client_credentials&scope=api
HTTP/1.1 OK
Cache-Control: no-store, no-cache, max-age=
Pragma: no-cache
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcdG9rZW4=?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: {"access_token":"5ac6b2bf4779873d0b92af8f32d6a3a85937cf5821056c1f9ca5435d9e717007","expires_in":,"token_type":"Bearer"}
调用 API 产生的报文
GET http://localhost:17181/api/values/1 HTTP/1.1
Authorization: Bearer 5ac6b2bf4779873d0b92af8f32d6a3a85937cf5821056c1f9ca5435d9e717007
Host: localhost: HTTP/1.1 OK
Content-Type: application/json; charset=utf-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5XZWJBcGlcYXBpXHZhbHVlc1wx?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: [{"type":"iss","value":"http://localhost:5000"},{"type":"nbf","value":""},{"type":"exp","value":""},{"type":"aud","value":"http://localhost:5000/resources"},{"type":"aud","value":"api"},{"type":"client_id","value":"client_credentials_reference_grant"},{"type":"active","value":"True"},{"type":"scope","value":"api"}] POST http://localhost:5000/connect/introspect HTTP/1.1
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length:
Host: localhost: token=5ac6b2bf4779873d0b92af8f32d6a3a85937cf5821056c1f9ca5435d9e717007&client_id=api&token_type_hint=access_token&client_secret=api_pwd
HTTP/1.1 OK
Cache-Control: no-store, no-cache, max-age=
Pragma: no-cache
Content-Type: application/json; charset=UTF-
Server: Kestrel
X-SourceFiles: =?UTF-?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcaW50cm9zcGVjdA==?=
X-Powered-By: ASP.NET
Date: Wed, Jul :: GMT
Content-Length: {"iss":"http://localhost:5000","nbf":,"exp":,"aud":["http://localhost:5000/resources","api"],"client_id":"client_credentials_reference_grant","active":true,"scope":"api"}
当 AccessTokenType 定义为 Reference 的时候,验证资源端要注意配置 ApiSecrets 以确保 POST /connect/introspect HTTP/1.1 接口能验证通过,当 AccessTokenType 定义为 Jwt 的时候则资源端可不配置 options.ApiSecret 选项。
//jwt
//services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
// .AddIdentityServerAuthentication(options =>
// {
// options.Authority = "http://localhost:5000";
// options.RequireHttpsMetadata = false;
// options.ApiName = "api";
// }); //Enable reference tokens
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api";
options.ApiSecret = "api_pwd";
});
问题总结
签名证书
OpenSSL 是不支持 window 的,可以在 Linux 上通过如下命令生成
openssl req -newkey rsa: -nodes -keyout idsrv4.key -x509 -days -out idsrv4.cer
openssl pkcs12 -export -in idsrv4.cer -inkey idsrv4.key -out idsrv4.pfx
完成后会有三个文件(选中配置文件设置始终复制)
root@iZuf60cj5pna5im3va46nlZ:~# tree
.
├── idsrv4.cer
├── idsrv4.key
└── idsrv4.pfx
配置文件
{
"Certs": {
"Path": "Certs\\idsrv4.pfx",
"Pwd": ""
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
最后访问 /.well-known/openid-configuration/jwks 确认
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "B4F7C5533A06B22E6D349BEFD84B76E730161B55",
"x5t": "tPfFUzoGsi5tNJvv2Et25zAWG1U",
"e": "AQAB",
"n": "zDXSeNo4oO-Tn372eKUywF40D0HG4XXeYtbYtdnpVsIZkDDouZr2jFeq3C-AUb546CJXFqqZj6YZPOMtiHBfzyDGThd45mQvNwQ18B7lae4vab1hvxx9HZGku64Wy5JlqT2jHJ-WR7GS9OZjHSeioMoDE654LhDxJthfj_C2G0jA_RTnPQKnQgciv5JiENTUwrghr9cXzBNgPE0QLAhKrCEoVoSxYOWTL9EBCUc2DB2Vah7RHNfNItrXbrdqvrDQ5rXBH8Rq6irjSF_FjcuIwMkTmLOkswnC_qBN7qjbmgLRIxG3YiSnZR5bgyhjFWNzea0jmuWEiFIIIMwTfPXpPw",
"x5c": [
"MIID8TCCAtmgAwIBAgIJAIRTKytMROvuMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTAeFw0xODA3MjUwNzI3MzZaFw0xOTA3MjUwNzI3MzZaMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMw10njaOKDvk59+9nilMsBeNA9BxuF13mLW2LXZ6VbCGZAw6Lma9oxXqtwvgFG+eOgiVxaqmY+mGTzjLYhwX88gxk4XeOZkLzcENfAe5WnuL2m9Yb8cfR2RpLuuFsuSZak9oxyflkexkvTmYx0noqDKAxOueC4Q8SbYX4/wthtIwP0U5z0Cp0IHIr+SYhDU1MK4Ia/XF8wTYDxNECwISqwhKFaEsWDlky/RAQlHNgwdlWoe0RzXzSLa1263ar6w0Oa1wR/Eauoq40hfxY3LiMDJE5izpLMJwv6gTe6o25oC0SMRt2Ikp2UeW4MoYxVjc3mtI5rlhIhSCCDME3z16T8CAwEAAaNQME4wHQYDVR0OBBYEFOK5Y2P7/L8KsOrPB+glPVkKi2VOMB8GA1UdIwQYMBaAFOK5Y2P7/L8KsOrPB+glPVkKi2VOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEnXXws/cBx5tA9cBfmkqGWzOU5/YmH9pzWchJ0ssggIqZVx0yd6ok7+C+2vKIRMp5E6GCfXWTB+LI7qjAVEvin1NwGZ06yNEsaYaJYMC/P/0TunoMEZmsLM3rk0aISbzkNciF+LVT16i0C+hT1+Pyr8lP4Ea1Uw0n50Np6SOwQ6e2PMFFOIaqjG94tuCN3RX819IJSQPbq9FtRmNvmbWPM1v2CO6SYT51SvsIHnZyn0rAK+h/hywVQqmI5ngi1nErIQEqybkZj00OhmYpAqsetWYU5Cs1qhJ70kktlrd+jMHdarVB9ko0h+ij6HL22mmBYAb7zVGWyDroNJVhEw6DA="
],
"alg": "RS256"
}
]
}
IdentityServer QuickStart-UI 问题
默认 IdentityServer4 是没有登录授权相关页面的,可以在 https://github.com/IdentityServer/IdentityServer4.Quickstart.UI/tree/release 下载后复制到我们的项目中即可。
Token 的类型
默认 Token 的类型有 Jwt 与 Reference 。
Self-contained Json Web Token(默认使用 RFC 7519 - JSON Web Token (JWT))
API 端(资源服务器)收到第一个请求后(仅第一次请求,后续的请求都使用第一次获得的公钥进行验证),会去 IdentityServer 服务端调用/.well-known/openid-configuration/jwks 接口获取 RSA 公钥验签以确认 token 是否合法。另外使用 JWT 的方式 token 是不可撤销的(POST/connect/revocation)。
比如上述获得 token 可以在 https://jwt.io 网站中查看 JWT 组成
Reference token
API 端(资源服务器)需要每次去访问 IdentityServer 的 token 验证接口(POST /connect/introspect),当然 API 端也可以配置一定的时间来缓存结果,以减少验证的频率。
重写的接口
一般会重写 IResourceOwnerPasswordValidator 与 IProfileService 接口来验证用户与自定义一些功能。
数据的持久化
https://identityserver4.readthedocs.io/en/release/reference/ef.html
http://blog.stoverud.no/posts/identity-server-with-mongodb/
REFER:
https://github.com/IdentityServer/IdentityServer4
http://docs.identityserver.io/en/release/index.html
https://github.com/Microsoft/api-guidelines
https://github.com/dotnet-architecture/eShopOnContainers
https://leastprivilege.com/2016/01/17/which-openid-connectoauth-2-o-flow-is-the-right-one/
https://identityserver4.readthedocs.io/en/release/reference/ef.html
http://blog.stoverud.no/posts/identity-server-with-mongodb/
https://github.com/ddrsql/IdentityServer4.Adminhttps://github.com/skoruba/IdentityServer4.Admin
IdentityServer 4 has changed and replaced IUserService with IResourceOwnerPasswordValidator and IProfileService
https://stackoverflow.com/questions/35304038/identityserver4-register-userservice-and-get-users-from-database-in-asp-net-core
http://www.cnblogs.com/skig/p/6079457.html
http://www.cnblogs.com/xishuai/p/identityserver4-slb.html
http://www.tugberkugurlu.com/archive/asp-net-core-authentication-in-a-load-balanced-environment-with-haproxy-and-redis
https://github.com/jessetalk/aspnet-core-in-practise/blob/master/chapter1.md
http://www.cnblogs.com/RainingNight/p/oidc-authentication-in-asp-net-core.html
https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf
https://github.com/ory/hydra
http://www.cnblogs.com/skig/p/AspNetCoreAuthCode.htmlhttps://github.com/nginxinc/nginx-openid-connect
使用 IdentityServer4 实现 OAuth 2.0 与 OpenID Connect 服务的更多相关文章
- 微服务系列之授权认证(一) OAuth 2.0 和 OpenID Connect
1.传统架构的授权认证 传统应用架构,用户使用账号密码登录后,可以使用前端cookie存储登录状态,也可以使用后端session方式存储登录状态,小应用这么做其实很高效实用,当应用需要横向扩展时,就需 ...
- ASP.NET 中OAUTH 2.0 及OPENID CONNECT的介绍
了解以下内容对ASP.NET 5中的验证中间件应用有很大帮助! OAUTH2是目前很多大型网站都使用的对外提供开放资源接口的应用标准,比入taobao\alipay\腾讯\豆瓣等.它和目前的另 ...
- .Net Core身份认证:IdentityServer4实现OAuth 2.0 客户端模式 - 简书
原文:.Net Core身份认证:IdentityServer4实现OAuth 2.0 客户端模式 - 简书 一.客户端模式介绍 客户端模式(Client Credentials Grant)是指客户 ...
- ASP.NET没有魔法——ASP.NET OAuth、jwt、OpenID Connect
上一篇文章介绍了OAuth2.0以及如何使用.Net来实现基于OAuth的身份验证,本文是对上一篇文章的补充,主要是介绍OAuth与Jwt以及OpenID Connect之间的关系与区别. 本文主要内 ...
- ASP.NET OAuth、jwt、OpenID Connect
ASP.NET OAuth.jwt.OpenID Connect 上一篇文章介绍了OAuth2.0以及如何使用.Net来实现基于OAuth的身份验证,本文是对上一篇文章的补充,主要是介绍OAuth与J ...
- IdentityServer4 实现 OAuth 2.0(密码模式 - HTTP Post 方式)
之前写了一篇文章:<IdentityServer4 实现 OpenID Connect 和 OAuth 2.0> 上面这篇文章虽然详细,但都是点到为止的介绍,并没有实际应用的示例,所以,后 ...
- IdentityServer4【QuickStart】之利用OpenID Connect添加用户认证
利用OpenID Connect添加用户认证 利用OpenID Connect添加用户认证 在这个示例中我们想要通过OpenID Connect协议将交互用户添加到我们的IdentityServer上 ...
- PHP下的Oauth2.0尝试 - OpenID Connect
OpenID Connect OpenID Connect简介 OpenID Connect是基于OAuth 2.0规范族的可互操作的身份验证协议.它使用简单的REST / JSON消息流来实现,和之 ...
- .Net Core身份认证:IdentityServer4实现OAuth 2.0 客户端模式
一.客户端模式介绍 客户端模式(Client Credentials Grant)是指客户端直接向认证服务(Authorization Server)发送认证请求,获取token,进行认证,一般适用于 ...
随机推荐
- Linux学习笔记:nginx基础
nginx [engine x] is an HTTP and reverse proxy server, a mail proxy server, and a generic TCP/UDP pro ...
- linux中tomcat startup.sh出现commond not found
问题: 前些天,再Linux提交更新代码启动tomcat时报commond not found 过程: 查了下百度,http://code2care.org/2015/-bash:-startup.s ...
- 通过PRINT过程制作报表
通过PRINT过程制作报表 PRINT过程是SAS中用于输出数据集内容的最简单常用的过程,它可将选择的观测和字段以简单的矩形表格形式输出. 1.1 制作简单报表 使用PRINT过程最简单的语法形式如下 ...
- windows 多网卡路由设置
1.问题 windows操作系统,双网卡连接内外网,配置路由使内网请求走内网网卡,外网请求走外网网卡 2.网络参数 内网网卡 IP:172.22.25.152 mask: 255.255.255.0 ...
- 2019.03.09 bzoj5371: [Pkusc2018]星际穿越(主席树)
传送门 题意简述: 给一个序列,对于第iii个位置,它跟[limi,i−1][lim_i,i-1][limi,i−1]这些位置都存在一条长度为111的无向边. 称dist(u,v)dist(u,v) ...
- C语言 指针基础篇 数组,函数与指针的运用 2 14
下面看看如何在函数中运用指针吧 下面是往函数传入指针的简单操作,不是传入数组的.判断一个a是否大于b是的话给,是的话对其进行操作,不是的话就直接返回. #include <stdio.h> ...
- Unity3D编辑器扩展(六)——模态窗口
前面我们已经写了5篇关于编辑器的,这是第六篇,也是最后一篇: Unity3D编辑器扩展(一)——定义自己的菜单按钮 Unity3D编辑器扩展(二)——定义自己的窗口 Unity3D编辑器扩展(三)—— ...
- azure cosmos db (mongo DB)
使用.net mongo的操作类操作azure(微软云)cosmosdb时,发现在做delete的操作的时候可以传一个文档对象,但是最后这个文档会解析成具体的sql语句,而当文档特别大时这样就出先了转 ...
- hightopo自己用开源的方案重构一遍
经过一年多的学习吧前面路上的坑基本算踩过一遍了 所以下面计划吧hightopo网站上的demo用自己的方式重新写一遍
- 闵可夫斯基和(Mincowsky sum)
一.概述 官方定义:两个图形A,B的闵可夫斯基和C={a+b|a∈A,b∈B}通俗一点:从原点向图形A内部的每一个点做向量,将图形B沿每个向量移动,所有的最终位置的并便是闵可夫斯基和(具有交换律) 例 ...