从上一篇关于资源密码凭证模式中,经过使用client_id和client_secret以及用户名密码经过应用Client(客户端)直接获取,从而请求获取受保护的资源,可是这种方式存在client可能存了用户密码这不安全性问题,因此须要作到client是高可信的应用。所以,咱们能够考虑经过其余方式来解决这个问题。javascript
咱们经过Oauth2.0的简化受权模式了解到,可使用这种方式来解决这个问题,让用户本身在IdentityServer服务器进行登陆验证,客户端不须要知道用户的密码,从而实现用户密码的安全性。html
因此在这一篇中,咱们将经过多种受权模式中的简化受权模式进行说明,主要针对介绍IdentityServer保护API的资源,简化受权访问API资源。前端
有些 Web 应用是纯前端应用,没有后端,必须将令牌储存在前端。RFC 6749 就规定了这种方式,容许直接向前端颁发令牌。这种方式没有受权码这个中间步骤,因此称为(受权码)"简化"(implicit)。java
简化模式(implicit grant type)不经过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"受权码"这个步骤(受权码模式后续会说明)。全部步骤在浏览器中完成,令牌对访问者是可见的,且客户端不须要认证。git
这种方式把令牌直接传给前端,是很不安全的。所以,只能用于一些安全要求不高的场景,而且令牌的有效期必须很是短,一般就是会话期间(session)有效,浏览器关掉,令牌就失效了。github
这种模式的使用场景是基于浏览器的应用数据库
这种模式基于安全性考虑,建议把token时效设置短一些, 不支持refresh tokenc#
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI --->| | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | |<---(C)--- Redirection URI ----<| | | | with Access Token +---------------+ | | in Fragment | | +---------------+ | |----(D)--- Redirection URI ---->| Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |<---(E)------- Script ---------<| | | | +---------------+ +-|--------+ | | (A) (G) Access Token | | ^ v +---------+ | | | Client | | | +---------+
简化受权流程描述后端
(A)客户端携带客户端标识以及重定向URI到受权服务器;api
(B)用户确认是否要受权给客户端;
(C)受权服务器获得许可后,跳转到指定的重定向地址,并将令牌也包含在了里面;
(D)客户端不携带上次获取到的包含令牌的片断,去请求资源服务器;
(E)资源服务器会向浏览器返回一个脚本;
(F)浏览器会根据上一步返回的脚本,去提取在C步骤中获取到的令牌;
(G)浏览器将令牌推送给客户端。
参数 | 是否必须 | 含义 |
---|---|---|
response_type | 必需 | 表示受权类型,此处的值固定为"token" |
client_id | 必需 | 客户端ID |
redirect_uri | 可选 | 表示重定向的URI |
scope | 可选 | 表示受权范围。 |
state | 可选 | 表示随机字符串 |
(1)资源服务器生成受权URL并将用户重定向到受权服务器
(用户的操做:用户访问https://resourcesServer/index.html跳转到登陆地址,选择受权服务器方式登陆)
在受权开始以前,它首先生成state参数(随机字符串)。client端将须要存储这个(cookie,会话或其余方式),以便在下一步中使用。
第一步,A 网站提供一个连接,要求用户跳转到 B 网站,受权用户数据给 A 网站使用。
https://oauth2Server/oauth2/default/v1/authorize? response_type=token &client_id=${clientId} &redirect_uri=https://resourcesServer/implicit.html &scope=受权范围 &state=随机字符串
生成的受权URL如上所述(如上),请求这个地址后重定向访问受权服务器,其中 response_type参数为token,表示直接返回令牌。
(2)验证受权服务器登录状态
(用户的操做:若是未登录用帐号 User,密码12345登录https://oauth2Server/login,若是已登录受权服务器不须要此步骤)
若是未登录帐号,自动跳转到受权服务器登录地址,登录受权服务器之后用户被重定向client端
https://resourcesServer/implicit.html
如已提早登录受权服务器或受权服务器登录会话还存在自动重定向到client端
https://resourcesServer/implicit.html
(3)验证状态参数
(用户的操做:无需操做)
用户被重定向回客户机,URL中如今有一个片断包含访问令牌以及一些其余信息。
用户跳转到 B 网站,登陆后赞成给予 A 网站受权。这时,B 网站就会跳回redirect_uri
参数指定的跳转网址,而且把令牌做为 URL 参数,传给 A 网站。
https://resourcesServer/authorization-code.html \#access_token=&token_type=Bearer&expires_in=3600&scope=photo&state=随机字符串
其中,token参数就是令牌,A网站所以直接在前端拿到令牌。
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是由于 OAuth 2.0 容许跳转网址是 HTTP 协议,所以存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减小了泄漏令牌的风险。
用户使用这个令牌访问资源服务器,当令牌失效时使用刷新令牌去换取新的令牌
在示例实践中,咱们将建立一个受权访问服务,定义一个MVC客户端,MVC客户端经过IdentityServer上请求访问令牌,并使用它来访问API。
搭建认证受权服务
IdentityServer4
程序包
创建配置内容文件Config.cs
public static class Config { public static IEnumerable<IdentityResource> IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; public static IEnumerable<ApiScope> ApiScopes => new ApiScope[] { new ApiScope("Implicit_scope1") }; public static IEnumerable<ApiResource> ApiResources => new ApiResource[] { new ApiResource("api1","api1") { Scopes={ "Implicit_scope1" }, ApiSecrets={new Secret("apipwd".Sha256())} //api密钥 } }; public static IEnumerable<Client> Clients => new Client[] { new Client { ClientId = "Implicit_client", ClientName = "Implicit Auth", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris ={ "http://localhost:5002/signin-oidc", //跳转登陆到的客户端的地址 }, PostLogoutRedirectUris ={ "http://localhost:5002/signout-callback-oidc",//跳转登出到的客户端的地址 }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "Implicit_scope1" }, // 是否须要赞成受权 (默认是false) RequireConsent=true }, }; }
RedirectUris
: 登陆成功回调处理的客户端地址,处理回调返回的数据,能够有多个。
PostLogoutRedirectUris
:跳转登出到的客户端的地址。这两个都是配置的客户端的地址,且是identityserver4组件里面封装好的地址,做用分别是登陆,注销的回调
由于是简化受权的方式,因此咱们经过代码的方式来建立几个测试用户。
新建测试用户文件TestUsers.cs
public class TestUsers { public static List<TestUser> Users { get { var address = new { street_address = "One Hacker Way", locality = "Heidelberg", postal_code = 69118, country = "Germany" }; return new List<TestUser> { new TestUser { SubjectId = "1", Username = "i3yuan", Password = "123456", Claims = { new Claim(JwtClaimTypes.Name, "i3yuan Smith"), new Claim(JwtClaimTypes.GivenName, "i3yuan"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "i3yuan@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"), new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) } } }; } } }
返回一个TestUser的集合。
经过以上添加好配置和测试用户后,咱们须要将用户注册到IdentityServer4服务中,接下来继续介绍。
在startup.cs中ConfigureServices方法添加以下代码:
public void ConfigureServices(IServiceCollection services) { var builder = services.AddIdentityServer() .AddTestUsers(TestUsers.Users); //添加测试用户 // in-memory, code config builder.AddInMemoryIdentityResources(Config.IdentityResources); builder.AddInMemoryApiScopes(Config.ApiScopes); builder.AddInMemoryApiResources(Config.ApiResources); builder.AddInMemoryClients(Config.Clients); // not recommended for production - you need to store your key material somewhere secure builder.AddDeveloperSigningCredential(); }
在startup.cs中Configure方法添加以下代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); }
以上内容是快速搭建简易IdentityServer项目服务的方式。
这搭建 Authorization Server 服务跟上一篇资源密码凭证模式有何不一样之处呢?
- 在Config中配置客户端(client)中定义了一个
AllowedGrantTypes
的属性,这个属性决定了Client能够被哪一种模式被访问,GrantTypes.Implicit为简化受权。因此在本文中咱们须要添加一个Client用于支持简化受权(implicit)。- 简化受权不经过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,全部步骤在浏览器中完成,因此须要配置对应的回调地址和登出地址。这也是不一样于以前的资源全部者凭证模式。
实现对客户端认证受权访问资源
IdentityServer4.AccessTokenValidation 包
要将对 OpenID Connect 身份认证的支持添加到MVC应用程序中。
在startup.cs中ConfigureServices方法添加以下代码:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddAuthorization(); services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "http://localhost:5001"; options.RequireHttpsMetadata = false; options.ClientId = "Implicit_client"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; }); }
AddAuthentication
注入添加认证受权,当须要用户登陆时,使用cookie
来本地登陆用户(经过“Cookies”做为DefaultScheme
),并将DefaultChallengeScheme
设置为“oidc”,使用
AddCookie
添加能够处理 cookie 的处理程序。由于简化模式的实现是就是
OpenID Connect
,因此在AddOpenIdConnect
用于配置执行OpenID Connect
协议的处理程序。Authority
代表以前搭建的 IdentityServer 受权服务地址。而后咱们经过ClientId
。识别这个客户端。SaveTokens
用于在 cookie 中保留来自IdentityServer 的令牌。
而后要确保认证服务执行对每一个请求的验证,加入UseAuthentication
和UseAuthorization
到Configure
中,在startup.cs中Configure方法添加以下代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
UseAuthentication将身份验证中间件添加到管道中;
UseAuthorization 将启动受权中间件添加到管道中,以便在每次调用主机时执行身份验证受权功能。
在HomeController控制器并添加[Authorize]
特性到其中一个方法。在进行请求的时候,需进行认证受权经过后,才能进行访问。
[Authorize] public IActionResult Privacy() { ViewData["Message"] = "Secure page."; return View(); }
还要修改主视图以显示用户的Claim以及cookie属性。
@using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
访问 Privacy 页面,跳转到认证服务地址,进行帐号密码登陆,Logout 用于用户的注销操做。
在Chrome浏览器中,进行认证受权的时候,用户登陆以后,没法跳转到原网页,仍是停留在登陆页中,能够看控制台就发现上图的效果。
最后查找资料发现,是Google将于2020年2月份发布Chrome 80版本。本次发布将推动Google的“渐进改良Cookie”策略,打造一个更为安全和保障用户隐私的网络环境。因此本次更新可能致使浏览器没法向服务端发送Cookie。若是你有多个不一样域名的应用,部分用户颇有可能出现会话时常被打断的状况,还有部分用户可能没法正常登出系统。
因此咱们须要解决这个问题:
方法一:将域名升级为 HTTPS
方法二:使用代码修改 SameSite 设置
新增 SameSiteCookiesServiceCollectionExtensions 类 (能够下载源码查看)
private const SameSiteMode Unspecified = (SameSiteMode)(-1); 改成 private const SameSiteMode Unspecified = SameSiteMode.Lax;
若是没有域名或内网环境,可使用该方法,在 Startup 添加引用。
public IServiceProvider ConfigureServices(IServiceCollection services) { ... services.ConfigureNonBreakingSameSiteCookies(); ...