第一次寫博客,前幾天看到.netcore的認證,就心血來潮想實現一下基於netcore的一個掃一掃的功能,實現思路構思大概是web端通過cookie認證進行授權,手機端通過jwt授權,web端登錄界面通過signalr實現后端通訊,通過二維碼展示手機端掃描進行登錄.源碼地址:點我
話不多說上主要代碼,
在dotnetcore的startup文件中主要代碼
public void ConfigureServices(IServiceCollection services) { services.Configure<JwtSettings>(Configuration.GetSection("JwtSettings")); var jwtOptions = Configuration.GetSection("JwtSettings").Get<JwtSettings>(); services.AddAuthentication(o=> { o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer(o=> { o.TokenValidationParameters= new TokenValidationParameters { // Check if the token is issued by us. ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey)) }; }); services.AddMvc(); services.AddSignalR(); services.AddCors(options => { options.AddPolicy("SignalrPolicy", policy => policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod()); }); }
我們默認添加了一個cookie的認證用於web瀏覽器,之后又添加了基於jwt的一個認證,還添加了signalr的使用和跨域.
jwtseetings的配置文件為:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } }, "JwtSettings": { "Issuer": "http://localhost:5000", "Audience": "http://localhost:5000", "SecretKey": "helloword123qweasd" } }
Configure中的代碼為:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); //跨域支持 //跨域支持 app.UseCors("SignalrPolicy"); app.UseSignalR(routes => { routes.MapHub<SignalrHubs>("/signalrHubs"); }); app.UseAuthentication(); app.UseWebSockets(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
之后添加account控制器和login登錄方法:
我們默認使用內存來模擬數據庫;
//默認數據庫用戶 default database users public static List<LoginViewModel> _users = new List<LoginViewModel> { new LoginViewModel(){ Email="1234567@qq.com", Password="123"}, new LoginViewModel(){ Email="12345678@qq.com", Password="123"} };
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { var user = _users.FirstOrDefault(o => o.Email == model.Email && o.Password == model.Password); if (user != null) { var claims = new Claim[] { new Claim(ClaimTypes.Name,user.Email), new Claim(ClaimTypes.Role,"admin") }; var claimIdenetiy = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimIdenetiy)); return RedirectToLocal(returnUrl); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } } // If we got this far, something failed, redisplay form return View(model); }
默認進行了一個簡單的認證用戶是否存在存在的話就對其進行登錄簽入.
web端還有一個簡單的登出我就不展示了.
實現了web端的cookie認證后我們需要實現jwt的一個認證授權,我們新建一個控制器AuthorizeController,同樣的我們需要對其實現一個token的頒發
private JwtSettings _jwtOptions; public AuthorizeController(IOptions<JwtSettings> jwtOptions) { _jwtOptions = jwtOptions.Value; } // GET: api/<controller> [HttpPost] [Route("api/[controller]/[action]")] public async Task<IActionResult> Token([FromBody]LoginViewModel viewModel) { if(ModelState.IsValid) { var user=AccountController._users.FirstOrDefault(o => o.Email == viewModel.Email && o.Password == viewModel.Password); if(user!=null) { var claims = new Claim[] { new Claim(ClaimTypes.Name,user.Email), new Claim(ClaimTypes.Role,"admin") }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_jwtOptions.Issuer, _jwtOptions.Audience, claims, DateTime.Now, DateTime.Now.AddMinutes(30), creds); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } } return BadRequest(); }
這樣手機端的登錄授權功能已經實現了.手機端我們就用consoleapp來模擬手機端:
//模擬登陸獲取token HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/api/Authorize/Token"); var requestJson = JsonConvert.SerializeObject(new { Email = "1234567@qq.com", Password = "123" }); httpRequestMessage.Content = new StringContent(requestJson, Encoding.UTF8, "application/json"); var resultJson = httpClient.SendAsync(httpRequestMessage).Result.Content.ReadAsStringAsync().Result; token = JsonConvert.DeserializeObject<MyToken>(resultJson)?.Token;
通過手機端登錄來獲取token值用於之后的授權訪問.之后我們要做的事情就是通過app掃描二維碼往服務器發送掃描信息,服務端通過signalr調用web端自行登錄授權的功能.
服務端需要接受app掃描的信息代碼如下:
public class SignalRController : Controller { public static ConcurrentDictionary<Guid, string> scanQRCodeDics = new ConcurrentDictionary<Guid, string>(); private IHubContext<SignalrHubs> _hubContext; public SignalRController(IHubContext<SignalrHubs> hubContext) { _hubContext = hubContext; } //只能手機客戶端發起 [HttpPost, Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/[controller]/[action]")] public async Task<IActionResult> Send2FontRequest([FromBody]ScanQRCodeDTO qRCodeDTO) { var guid = Guid.NewGuid(); //scanQRCodeDics[guid] = qRCodeDTO.Name; scanQRCodeDics[guid] = User.Identity.Name; await _hubContext.Clients.Client(qRCodeDTO.ConnectionID).SendAsync("request2Login",guid); return Ok(); } }
public class ScanQRCodeDTO { [JsonProperty("connectionId")] public string ConnectionID { get; set; } [JsonProperty("name")] public string Name { get; set; } }
dto里面的數據很簡單(其實我們完全不需要name字段,你看我的signalr控制器已經注銷掉了),我展示的的做法是前段通過signalr-client鏈接后端服務器,會有一個唯一的connectionId,我們簡單地可以用這個connectionId來作為二維碼的內容,當然你可以添加比如生成時間或者其他一些額外的信息,方法Send2fontRequest被標記為jwt認證,所以該方法只有通過獲取jwt token的程序才可以訪問,字典我們用於簡單地存儲器,當手機端的程序訪問這個方法后,我們系統會生成一個隨機的guid,我們將這個guid存入剛才的存儲器,然后通過signalr調用前段方法,實現后端發起登錄,而不需要前段一直輪詢是否手機端已經掃碼這個過程.
<script src="~/js/jquery/jquery.qrcode.min.js"></script> <script src="~/scripts/signalr.js"></script> <script> $(function () { let hubUrl = 'http://localhost:5000/signalrHubs'; let httpConnection = new signalR.HttpConnection(hubUrl); let hubConnection = new signalR.HubConnection(httpConnection); hubConnection.start().then(function () { $("#txtqrCode").val(hubConnection.connection.connectionId); //alert(hubConnection.connection.connectionId); $('#qrcode').qrcode({ render: "table", // 渲染方式有table方式和canvas方式 width: 190, //默認寬度 height: 190, //默認高度 text: hubConnection.connection.connectionId, //二維碼內容 typeNumber: -1, //計算模式一般默認為-1 correctLevel: 3, //二維碼糾錯級別 background: "#ffffff", //背景顏色 foreground: "#000000" //二維碼顏色 }); }); hubConnection.on('request2Login', function (guid) { $.ajax({ type: "POST", url: "/Account/ScanQRCodeLogin", data: { uid: guid }, dataType: 'json', success: function (response) { console.log(response); window.location.href = response.url; }, error: function () { window.location.reload(); } }); }); }) </script>
這樣前段會收掉后端的一個請求並且這個請求只會發送給對應的connectionId,這樣我掃的那個客戶端才會執行登錄跳轉方法.
[HttpPost] [AllowAnonymous] public async Task<IActionResult> ScanQRCodeLogin(string uid) { string name = string.Empty; if (!User.Identity.IsAuthenticated && SignalRController.scanQRCodeDics.TryGetValue(new Guid(uid), out name)) { var user = AccountController._users.FirstOrDefault(o => o.Email == name); if (user != null) { var claims = new Claim[] { new Claim(ClaimTypes.Name,user.Email), new Claim(ClaimTypes.Role,"admin") }; var claimIdenetiy = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimIdenetiy)); SignalRController.scanQRCodeDics.TryRemove(new Guid(uid), out name); return Ok(new { Url = "/Home/Index" }); } } return BadRequest(); }
手機端我們還有一個發起請求的功能
//掃碼模擬 HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/api/SignalR/Send2FontRequest"); httpRequestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var requestJson = JsonConvert.SerializeObject(new ScanQRCodeDTO { ConnectionID = qrCode, Name = "1234567@qq.com" }); httpRequestMessage.Content = new StringContent(requestJson, Encoding.UTF8, "application/json"); var result = httpClient.SendAsync(httpRequestMessage).Result; var result1= result.Content.ReadAsStringAsync().Result; Console.WriteLine(result+",,,"+ result1);
第一次寫博客,可能排版不是很好,出於性能考慮我們可以將二維碼做成tab形式,如果你選擇手動輸入那么就不進行signalr鏈接,當你點到二維碼才需要鏈接到signalr,如果不需要使用signalr記得可以通過輪詢一樣可以達到相應的效果.目前signalr需要nuget通過勾選預覽版本才可以下載,大致就是這樣.