.net core 3.1 Identity Server4 (Implicit模式) 电脑版发表于:2020/12/18 16:55  >#.net core 3.1 Identity Server4 (Implicit模式) [TOC]  >### Implicit 模式的理解   tn>A.用户通过浏览器访问客户端,然后客户端跳转到授权服务器上。 B.用户输入用户和密码(授权信息) C.当用户输入的用户和密码有效的话,将跳转回指定的页面(这里根据客户端传过去Url做返回),并且带有Access Token。 D.跳入指定的链接,对Access Token以及其他参数做存储数据的处理(如存储到local storage中) E.存储完毕后,以前端脚本的形式跳回客户端 相对于Code来说:缺少了对客户端的验证,直接拿到了Access Token >### 创建客户端(AiDaSi.OcDemo.JavaScriptClient)   >修改`launchSettings.json`文件 ```bash { "profiles": { "AiDaSi.OcDemo.JavaScriptClient": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:6001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ``` >安装依赖包`Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation`,用来Razor视图和Razor页面的汇编。  >修改`Startup.cs` ```csharp public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews() .AddRazorRuntimeCompilation(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); } ``` >创建`Controllers/HomeController.cs`与`wwwroot`文件夹 ```csharp public class HomeController : Controller { public IActionResult Index() { return View(); } } ```  >### Implicit Flow tn>接着我们来看看`Implicit Flow`的请求格式,格式如下,参数意义看下表  | | | | | ------------ | ------------ | ------------ | | response_type | 必需要的 | 必须包括id_token用于OpenID Connect登录。它还可能包含response_type token。使用token此处将允许您的应用立即从授权端点接收访问令牌,而无需再次向授权端点发出请求。如果使用tokenresponse_type,则scope参数必须包含一个范围,该范围指示要为其发行令牌的资源(例如,Microsoft Graph上的user.read)。它也可以code代替token提供授权码而包含在授权码流中使用。这种id_token + code响应有时称为混合流。 | | client_id | 必需要的 | 页面分配给您的应用程序的应用程序(客户端)ID 。 | | redirect_uri | 必需要的 | 应用程序的redirect_uri,您的应用程序可以在其中发送和接收身份验证响应。它必须与您在门户网站中注册的redirect_uris之一完全匹配,但必须使用url编码。 | | scope | 必需要的 | 资源范围。 | | state | 推荐要得 | 请求中包含的值也将在令牌响应中返回。它可以是您希望的任何内容的字符串。通常使用随机生成的唯一值来防止跨站点请求伪造攻击。状态还用于在身份验证请求发生之前在应用程序中对有关用户状态的信息进行编码,例如用户所在的页面或视图。 | | nonce | 需要的 | 应用程序生成的请求中包含的值,该值将作为声明包含在生成的id_token中。然后,该应用可以验证该值以减轻令牌重放攻击。该值通常是一个随机的,唯一的字符串,可用于标识请求的来源。仅在请求id_token时才需要。 | 更多参数请参考:https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow >我们来按照上面的请求格式进行模拟创建请求,添加客户端中`Home/index.cshtml`视图,内容如下。  ```html h1>Javascript Client</h1> <button type="button" onclick="signIn()">登录</button> <script src="~/SignIn.js" type="text/javascript"></script> ``` >通过上一次的`Code`客户端模式做一次登录模拟请求,获取得到的地址如下:  >可以知道它的回调链接是`/connect/authorize/callback`,添加一个`SignIn.js`的js到`wwwroot`中,仿照一个链接如下: ```HTML // createState与createNonce随便填填就好了 var createState = function () { return "createStateasdfasdfasdfasdfasdfasd"; } var createNonce = function () { return "createNoncecreateNoncecreateNoncecreateNoncecreateNonce"; } var signIn = function () { // 跳回到客户端的/Home/SignIn地址 var redirectUri = "https://localhost:6001/Home/SignIn"; var responseType = "id_token token"; var scope = "openid ApiOne"; var authUrl = "/connect/authorize/callback" + "?client_id=js_client" + "&redirect_uri=" + encodeURIComponent(redirectUri) + "&response_type=" + encodeURIComponent(responseType) + "&scope=" + encodeURIComponent(scope) + "&nonce=" + createNonce() + "&state=" + createState(); var returnUrl = encodeURIComponent(authUrl); console.log(authUrl); console.log(returnUrl); // 我们在拼接好后访问 window.location.href = "https://localhost:7200/IdentityCodeAuth/Login?ReturnUrl=" + returnUrl; } ``` >接着我们访问试试..   >接着我们在授权服务器上添加`Implicit`授权客户端 ```csharp new Client { ClientId = "js_client", // Implicit AllowedGrantTypes = GrantTypes.Implicit, //登录成功后,跳转的地址 RedirectUris = { "https://localhost:6001/Home/SignIn" }, AllowedScopes = { "ApiOne", IdentityServerConstants.StandardScopes.OpenId, }, //允许浏览器访问令牌 AllowAccessTokensViaBrowser = true, //作用范围 RequireConsent = false } ``` >好的我们登录来试试,我们发现我们并没有添加图中所对应的页面,单里面返回了许多值这些值当中就有我们需要的`Access Token`。  >在客户端中添加`SignIn`方法,并创建对应的视图。访问成功后完成D步骤。 ```csharp public IActionResult SignIn() { return View(); } ``` ```html <h1>登录成功</h1> ```  >为返回的页面添加按钮,创建`sign-in-callback.js`文件来解码其中的值并添加到`LocalStorage`中去。 ```html <h1>登录成功</h1> <button onclick="extractTokens(window.location.href)">Extract Tokens</button> <script src="~/sign-in-callback.js"></script> ``` ```javascript var extractTokens = function (callbackUrl) { var returnValue = callbackUrl.split('#')[1]; var values = returnValue.split('&'); for (var i = 0; i < values.length; i++) { var v = values[i]; var kvPair = v.split('='); //将获取到的信息初入LocalStorage里面 localStorage.setItem(kvPair[0], kvPair[1]); } } ``` >此时我们获取到了我们所需要的一切  >修改`sign-in-callback.js`,然后我们可以将跳回到首页中去 ```javascript var extractTokens = function (callbackUrl) { var returnValue = callbackUrl.split('#')[1]; var values = returnValue.split('&'); for (var i = 0; i < values.length; i++) { var v = values[i]; var kvPair = v.split('='); //将获取到的信息初入LocalStorage里面 localStorage.setItem(kvPair[0], kvPair[1]); } //再返回到首页中去 window.location.href = '/home/index'; } var _callbackUrl = window.location.href; extractTokens(_callbackUrl) ``` >### Oidc-Client 简介 tn>oidc-client是一个JavaScript库,指在浏览器中运行。它为OIDC和OAuth2提供协议支持,以及用于用户会话和访问令牌管理的管理功能。 根据使用库的级别,可能要使用两个主要类:<br/> **1 .**本`UserManager`类为签约用户,注销,管理从OIDC提供程序返回的用户的声明,以及管理令牌从 OIDC/OAuth2 用户提供返回的接入更高级别的API。该`UserManager`是库的主要功能。<br/> **2 .**的`OidcClient`类提供的授权端点和在授权服务器结束会话端点原始 OIDC/OAuth2 用户协议的支持。它提供了基本的协议实现,并且由`UserManager`该类使用。仅在只希望协议支持而又没有`UserManager`该类的其他管理功能的情况下,才使用此类。 >### 属性设置表 tn>该`UserManager`构造函数需要一个设置对象作为参数。设置具有以下属性: | | | | ------------ | ------------ | | authority | string | OIDC/OAuth2 提供程序的URL。 | | client_id | string | 在 OIDC/OAuth2 提供程序中注册的客户端应用程序的标识符。 | | redirect_uri | string | 您的客户端应用程序的重定向URI,以接收来自 OIDC/OAuth2 提供程序的响应。 | | response_type | string, 默认为: `'id_token'` | OIDC/OAuth2 提供程序所需的响应类型。 | | scope | string, 默认为: `'openid'` | 从 OIDC/OAuth2 提供程序请求的范围。 | >### 使用OIDC-Client.js登录 tn>添加`main.js`文件到`wwwroot`中去 ```javascript var config = { authority: "https://localhost:7200/", // OIDC/OAuth2提供程序的URL。 client_id: "js_client", // 在 OIDC/OAuth2 提供程序中注册的客户端应用程序的标识符。 response_type: "id_token token", // 默认值'id_token' OIDC/OAuth2 提供程序所需的响应类型。 Type为Implicit时 redirect_uri: "https://localhost:6001/Home/SignIn", // 您的客户端应用程序的重定向URI,以接收来自 OIDC/OAuth2 提供程序的响应。 scope: 'ApiOne openid', // 从 OIDC/OAuth2 提供程序请求的范围。 } var userManger = new Oidc.UserManager(config); var signIn = function () { userManger.signinRedirect(); } ``` tn>修改`index.cshtml`文件的内容 ```javascript <h1>Javascript Client</h1> <button type="button" onclick="signIn()">登录</button> <script src="https://cdn.bootcdn.net/ajax/libs/oidc-client/1.9.1/oidc-client.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script> <script src="~/main.js"></script> ``` tn>在授权服务器中`js_client`客户端下面添加允许跨域的地址 ```csharp //跨域请求白名单 AllowedCorsOrigins = { "https://localhost:6001" }, ``` tn>修改登录成功后修改回调页面`SignIn.cshtml`的内容 ```html <h1>登录成功</h1> <script src="https://cdn.bootcdn.net/ajax/libs/oidc-client/1.9.1/oidc-client.min.js"></script> <script> var userManager = new Oidc.UserManager(); userManager.signinCallback().then(res => { console.log('处理已经完成,正在执行回调函数:', res); window.location.href = '/home/index'; }); </script> ``` tn>然后运行,成功走完登录流程,并输出了我们想要的数据。里面有`id_token`,`access_token`....  >### 请求API(ApIDemo1) tn>在API Startup中添加跨域政策与服务 ```csharp //添加跨域服务政策 services.AddCors(config => { config.AddPolicy("AllowAll", p => p.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() ); }); ``` ```csharp //使用跨域的中间件 app.UseCors("AllowAll"); ``` tn>在`main.js`添加将token加入到请求对象中与对API接口访问的功能。 ```javascript // 判断用户是否登录 userManger.getUser().then(user => { console.log("user:", user); if (user) { // 将token加入到axios请求对象中 axios.defaults.headers.common["Authorization"] = "Bearer " + user.access_token; } }); // 请求API接口 var callApi = function () { axios.get("http://localhost:5280/WeatherForecast") .then(res => { console.log(res); }); } ``` tn>在`index.cshtml`中,添加对接口的请求 ```html <div> <button type="button" onclick="callApi()">请求APIOne接口</button> </div> ``` tn>启动接口项目  tn>在登录后对接口进行访问,获取到接口信息  >### 设置用户信息存储的地方 tn>`userStore`用于为当前经过身份验证的用户保留用户的存储对象到`localstorage`,接下来我们为`main.js`中的`config`与`Signln.cshtml`中实例`UserManager`对象时添加下面的代码。 ```javascript userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }) ``` >### 刷新Token tn>在授权服务器中修改`Config`,设置如下 ```javascript new Client { ClientId = "js_client", // Implicit AllowedGrantTypes = GrantTypes.Implicit, //登录成功后,跳转的地址 RedirectUris = { "https://localhost:6001/Home/SignIn" }, //退出时跳转的地址 PostLogoutRedirectUris = { "https://localhost:6001/Home/Index" }, //跨域请求白名单 AllowedCorsOrigins = { "https://localhost:6001" }, AllowedScopes = { "ApiOne", IdentityServerConstants.StandardScopes.OpenId, }, //访问令牌的生命周期为30秒钟 AccessTokenLifetime = 30, //允许浏览器访问令牌 AllowAccessTokensViaBrowser = true, //作用范围 RequireConsent = false } ``` tn>在`main.js`的`config`中添加注销后的重定向地址 ```javascript post_logout_redirect_uri: "https://localhost:6001/Home/Index", ``` tn>当`Access Token`失效后,我们需要在js的客户端过滤器中进行刷新操作。在`main.js`中添加如下代码可达成自动刷新的效果。 ```javascript var refreshing = false; //输出拦截器 axios.interceptors.response.use( function (response) { return response; }, //当报错时刷新令牌 function (error) { console.log(" axios error: ", error.response); let axiosConfig = error.response.config; // 如果状态码是401我们将会刷新令牌 if (error.response.status === 401) { console.log(" axios error 401"); // 如果已经刷新,请不要再提出其他需求 if (!refreshing) { console.log("开始刷新"); refreshing = true; // 刷新令牌方法 return userManger.signinSilent().then(res => { console.log(res); // 更新请求Token与本地客户端的Token axios.defaults.headers.common['Authorization'] = "Bearer " + res.access_token; axiosConfig.headers['Authorization'] = "Bearer " + res.access_token; refreshing = false; // 并重试一下axios的请求 return axios(axiosConfig); }); } } // 带一个有拒绝原因的方法 return Promise.reject(error); }); ```  