实现基于dotnetcore的扫一扫登录功能

  第一次写博客,前几天看到.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通过勾选预览版本才可以下载,大致就是这样.

 

posted @ 2018-03-16 16:56  薛家明  阅读(2106)  评论(2编辑  收藏  举报