NetCore 基于Cookie的身份验证(Policy)

ASP.NET Core 认证系统,包括Cookie, JwtBearer, OAuth, OpenIdConnect等

下面我们要讲的就是基于Cookie的身份认证

由于HTTP协议是无状态的,但对于认证来说,必然要通过一种机制来保存用户状态,而最常用,也最简单的就是Cookie了,它由浏览器自动保存并在发送请求时自动附加到请求头中。尽管在现代Web应用中,Cookie已略显笨重,但它依然是最为重要的用户身份保存方式。

让我们来了解下什么是:身份

理解身份验证如何工作的关键是首先了解ASP.NET Core 2.0中的身份。有三个类代表用户的身份:

Claim:身份证单元,ClaimsIdentity:身份证和ClaimsPrincipal:身份证持有者

Microsoft.AspNetCore.All 是 ASP.NET Core 的全家桶,包含 Mvc, EFCore, Identity, NodeService, AzureAppServe 等等,并集成到了 .NET Core SDK 当中,个人感觉有违 .NET Core 轻量模块化的理念,虽然使用看似更加方便了,却也让我们变得更加傻瓜化。当然你可以移除掉 Microsoft.AspNetCore.All 的引用,然后手动安装需要的Nuget包,当然,你可以不这么做,并略过此小节

如果你移除掉Microsoft.AspNetCore.All 的引用,那么基于Cookie的身份验证就需要安装如下几个包

dotnet add package Microsoft.AspNetCore.Hosting --version 2.0.0 //ASP.NET Core应用程序的宿主程序,必须引用
dotnet add package Microsoft.AspNetCore.Server.Kestrel --version 2.0.0 //最常用的Web服务器,支持跨平台,在Windows下还可以使用HttpSysServer或IISIntegration
dotnet add package Microsoft.Extensions.Logging.Console --version 2.0.0 //用来记录日志,可将日志输出到控制台,调试窗口,Windows事件日志等
dotnet add package Microsoft.AspNetCore.Authentication.Cookies --version 2.0.0 //Cookies

在Microsoft.AspNetCore.Authentication 的namespace命名空间下有定义了一个 AuthenticationHttpContextExtensions

AuthenticationHttpContextExtensions 类是对 HttpContext 认证相关的扩展,它提供了如下扩展方法:

AuthenticateAsync:验证在 SignInAsync 中颁发的证书,并返回一个 AuthenticateResult 对象,表示用户的身份

ChallengeAsync:通知用户需要登录。在默认实现类AuthenticationHandler中,返回401

ForbidAsync:通知用户权限不足。在默认实现类AuthenticationHandler中,返回403

SignInAsync:登录用户。(该方法需要与AuthenticateAsync配合验证逻辑)  SignInAsync用户登录成功后颁发一个证书(加密的用户凭证,这个凭证放入Cookie中),用来标识用户的身份

SignOutAsync:退出登录,里面做的操作就是清除Cookie

项目实例:

控制器:AccountController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using WebApp.Models;

namespace WebApp.Controllers
{
    [AllowAnonymous]//允许匿名访问
    public class AccountController : Controller
    {      
        private DbService db;

        public AccountController(DbService _db)
        {
            this.db = _db;
        }

        /// <summary>
        /// 登陆视图界面
        /// </summary>
        /// <param name="returnUrl"></param>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Login(string returnUrl = null) //这个returnUrl一般用于做单点登陆的认证中心回调地址
        {
            TempData["returnUrl"] = returnUrl;
            return View();
        }
        /// <summary>
        /// 登陆Post请求
        /// </summary>
        /// <param name="model"></param>
        /// <param name="returnUrl"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)//这个returnUrl同上
        {
            const string errorMsg = "用户名或密码错误";
            if (model == null)
            {
                //ModelState.AddModelError(string.Empty,errorMsg);
                return BadRequest(errorMsg);
            }
            var userInfo = new DbService().FindUser(model.UserName, model.Password); //查询数据库
            if (userInfo == null)
            {
                //ModelState.AddModelError(string.Empty, errorMsg);
                return BadRequest(errorMsg);
            }

            var roles = db.GetRolesByUserId(userInfo.Id);

            //创建身份证的证件单元:一张身份证由许多证件单元组成
            List<Claim> claims = new List<Claim>() {
                                new Claim(ClaimTypes.Sid, userInfo.Id.ToString()),
                                new Claim(ClaimTypes.Name, userInfo.Name),
                                new Claim(ClaimTypes.Email, userInfo.Email),
                                new Claim(ClaimTypes.MobilePhone, userInfo.PhoneNumber),
                                new Claim(ClaimTypes.Country, userInfo.Source),//来源:QQ,微信,微博之类的
                               
                            };
            roles.ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r)));

            //根据证件单元创建身份证
            ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

            //创建身份证这个证件的携带者:我们叫这个证件携带者为“证件当事人”
            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal();
            claimsPrincipal.AddIdentity(identity); //将身份证交给到“证件当事人”手上

            //这一步就是用身份证信息创建了一个加密的Cookie,然后这个Cookie会发送到浏览器中,这叫保存登陆状态(其实我觉得更应该叫Token)
            //然后浏览器再将这个Cookie发送服务端,服务端解析这个Cookie。
            await HttpContext.SignInAsync(claimsPrincipal);


            if (returnUrl == null)
            {

                returnUrl = TempData["returnUrl"]?.ToString();
            }

            if (returnUrl != null)//&& !Url.IsLocalUrl(returnUrl)
            {
                return Redirect(returnUrl);
            }
            return RedirectToAction(nameof(HomeController.Index), "Home");


        }

        /// <summary>
        /// 无权限
        /// </summary>
        /// <param name="returnUrl"></param>
        /// <returns></returns>
        public IActionResult AccessDenied(string returnUrl = null)
        {
            return View();
        }

        /// <summary>
        /// 登出
        /// </summary>
        /// <returns></returns>
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return RedirectToAction("Login");
        }



        / <summary>
        / 注册
        / </summary>
        / <param name="model"></param>
        / <param name="returnUrl"></param>
        / <returns></returns>
        //public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
        //{
        //    returnUrl = returnUrl ?? Url.Content("~/");
        //    if (ModelState.IsValid)
        //    {
        //        var user = new User { Name = model.Name, Email = model.Name };
        //        //var result = await _userManager.CreateAsync(user, model.Password);

        //        var result = new { Succeeded = true, Errors = string.Empty };//这一步是将注册用户写入数据库,我就不写了,模拟已经注册成功了
        //        if (result.Succeeded)
        //        {

        //            //为指定用户生成电子邮件确认令牌。
        //            var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
        //            var callbackUrl = Url.Page(
        //                "/Account/ConfirmEmail",
        //                pageHandler: null,
        //                values: new { userId = user.Id, code = code },
        //                protocol: Request.Scheme);

        //            //await EmailHelper.SendEmailAsync(Input.Email, "Confirm your email",
        //            //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

        //            //登陆操作;
        //            var Identity = new ClaimsIdentity();
        //            Identity.AddClaim(new Claim(ClaimTypes.Name, model.Name));
        //            await HttpContext.SignInAsync(new ClaimsPrincipal(Identity));

        //            if (returnUrl != null) //Url不等于null一般情况是单点登录注册
        //            {
        //                return Redirect(returnUrl);
        //            }
        //            return RedirectToAction(nameof(HomeController.Index), "Home");

        //        }
        //        foreach (var error in result.Errors)
        //        {
        //            ModelState.AddModelError(string.Empty, "Error中的错误信息");
        //        }
        //    }
        //    return RedirectToAction("Error");
        //}
    }
}

控制器:HomeController.cs

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using WebApp.Models;

namespace WebApp.Controllers
{
    //[AllowAnonymous]
    public class HomeController : Controller
    {

        private IAuthorizationService _authorization;
        public HomeController(IAuthorizationService authorizationService)
        {
            _authorization = authorizationService;
        }

        [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] //Schemes授权
        //[Authorize(Roles = "Sale,Admin")] //这个Action只能是Sale和Admin这个角色才能访问 //Role授权
        [Authorize(Policy = "MyPolicy")]
        public async Task<IActionResult> Index()
        {
            //var allowed = await _authorization.AuthorizeAsync(User, "MyPolicy");
            var user = User.FindFirst(ClaimTypes.Name).Value;
            var role = User.FindFirst(ClaimTypes.Role).Value;
            HttpContext.Response.Cookies.Append("UserName", "wowo");

            IEnumerable<ClaimsIdentity> u = User.Identities;
            var d = u.ToString();
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }



        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
  
}

在项目下创建一个Authentication,在文件夹中创建一个MyPolicyRequirement类,这个类自定义验证方式

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using System.Security.Claims;
using System.Threading.Tasks;

namespace WebApp
{  
    public class MyPolicyRequirement : IAuthorizationRequirement
    {
        
    }

    public class MyPolicyHandler : AuthorizationHandler<MyPolicyRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyPolicyRequirement requirement)
        {
            //这里我们可以自由的发挥我们的定义的授权验证

            //在这里我们可以要求Claim中必须要有Sid这个属性
            //if (context.User.HasClaim(c => c.Type == ClaimTypes.Sid))
            //{
            //    string userIdStr = context.User.FindFirst(c => c.Type == ClaimTypes.Sid).Value;
            //    int userId = Convert.ToInt32(userIdStr);


            //    if (userId < 2)
            //    {
            //        context.Succeed(requirement);
            //    }
            //}
            if (context.User != null && context.User.HasClaim(c => c.Type == ClaimTypes.Country))
            {
                //自定义授权策略:只有来源为QQ的用户能访问
                string source = context.User.FindFirst(ClaimTypes.Country).Value;
                if (source == "WeChat")
                {
                    context.Succeed(requirement);
                }
            }
            return Task.CompletedTask;
        }
    }
}

创建一个类:DbService.cs

using System.Collections.Generic;
using System.Linq;
using WebApp.Models;

namespace WebApp
{
    public class DbService
    {
        /// <summary>
        /// 模拟用户数据库表:假设数据库中有两条数据,用来实现用户登录时的用户名和密码的检查
        /// </summary>
        private List<User> users = new List<User>()
        {
            new User {  Id=1, Name="lily", Password="123456", Email="lily@gmail.com", PhoneNumber="18600000001" , Source="QQ" },
            new User {  Id=2, Name="jeck", Password="123456", Email="jeck@gmail.com", PhoneNumber="18600000002",Source="WeChat" },
            new User {  Id=3, Name="tom", Password="123456", Email="tom@gmail.com", PhoneNumber="18600000002",Source="MicroBlog" }
        };

        public List<Role> role = new List<Role>()
        {
            new Role{ Id=1,RoleName="Admin", RoleDescribe="系统管理员" },
            new Role{ Id=2,RoleName="Sale", RoleDescribe="销售" },
            new Role{ Id=3,RoleName="Finance", RoleDescribe="财务" },
        };

        private List<UserRole> userRole = new List<UserRole>()
        {
            new UserRole{ UserId=1, RoleId=1 }, //lily是系统管理员
            new UserRole{ UserId=2, RoleId=2 }, //jeck是销售
            new UserRole{ UserId=2, RoleId=3 }, //jeck又是财务
            new UserRole{ UserId=3, RoleId=2 }  //tom是销售
        };

        public User FindUser(string userName, string password)
        {
            return users.FirstOrDefault(r => r.Name == userName  && r.Password == password);            
        }
        //根据用户ID查询用户角色集合
        public List<string> GetRolesByUserId(int userid)
        {
            List<string> roleNames = new List<string>();
            List<int> roleids = userRole.Where(r => r.UserId == userid).Select(r => r.RoleId).ToList();
            role.ForEach(r =>
            {
                if (roleids.Contains(r.Id))
                {
                    roleNames.Add(r.RoleName);
                }
            });
            return roleNames;
        }

        public string GetRoles(int userid)
        {
            List<string> roleNames = GetRolesByUserId(userid);
            return string.Join(",", roleNames);
        }
    }
}

Startup.cs类

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using WebApp.Models;

namespace WebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }


        public void ConfigureServices(IServiceCollection services)
        {
           
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;//CheckConsentNeeded设置为true,非必需 cookie 不会发送到浏览器

            });
            
            启用我们的自定义的MyAuthenticationHandler身份验证中心
            //services.AddAuthenticationCore(o =>
            //{
            //    o.AddScheme<MyAuthenticationHandler>("myScheme", "描述信息而已");                
            //});

            //Authentication:是认证。认证是用来证明一个人的身份(如:姓名,邮箱等等),一般情况是判断你这个人是否登陆过,登陆后保存了Cookie自然就知道你的身份了
            services.AddAuthentication(o =>
            {               
                o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//使用默认的Scheme认证授权Scheme

            }).AddCookie(o =>
            {
                //关于Cookie的配置信息请参考:https://www.cnblogs.com/sheldon-lou/p/9545726.html

                //o.Cookie.Domain = ".contoso.com";//设置Cookie的作用域:他的作用域就包括contoso.com,www.contoso.com

                o.LoginPath = "/Account/Login"; //在身份验证的时候判断为“未登录”则跳转到这个页面
                o.LogoutPath = "/Account/Logout";//如果要退出登录则跳转到这个页面
                o.AccessDeniedPath = "/Account/AccessDenied"; //如果已经通过身份验证,但是没有权限访问则跳转到这个页面

               
                o.Cookie.HttpOnly = true;//设置 cookie 是否是只能被服务器访问,默认 true,为true时通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性
                o.SlidingExpiration = true;//设置Cookie过期时间为相对时间;也就是说在Cookie设定过期的这个时间内用户没有访问服务器,那么cookie就会过期,若有访问服务器,那么cookie期限将从新设为这个时间
                o.ExpireTimeSpan = TimeSpan.FromDays(1); //设置Cookie过期时间为1天

                
                o.ClaimsIssuer = "Cookie";//获取或设置应用于创建的任何声明的颁发者

                //o.Cookie.Path = "/app1"; //用来隔离同一个服务器下面的不同站点。比如站点是运行在/app1下面,设置这个属性为/app1,那么这个 cookie 就只在 app1下有效。


            });

            //Authorization:是授权。这里是置授权策略;授权是用来表示这个用户能做什么事情,比如admin可以修改删除数据,normal user只能查看数据。
            services.AddAuthorization(options =>
            {
                options.AddPolicy("MyPolicy",
                    policy =>
                    {
                        policy.Requirements.Add(new MyPolicyRequirement());
                        //policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
                        //policy.RequireAuthenticatedUser();
                        //policy.RequireRole("Admin", "Finance");//使用角色配置策略,多个系统角色
                        //policy.RequireClaim("claimname"); //将ClaimsAuthorizationRequirement添加到当前实例
                        //policy.RequireUserName("username");
                    });
            });

            services.AddSingleton<IAuthorizationHandler, MyPolicyHandler>();//单例:依赖注入
            services.AddSingleton<DbService>(); //将DbService注册到DI系统中


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }


        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {           

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseAuthentication();//支持授权
            //默认情况下,ASP.NET Core 应用不会为 HTTP 状态代码(如“404 - 未找到”)提供状态代码页。 应用返回状态代码和空响应正文。 若要提供状态代码页,请使用状态代码页中间件。
            //app.UseStatusCodePages();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

当然我们也可以自定义验证中心

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System;
using System.Security.Claims;
using System.Threading.Tasks;

namespace WebApp.Authentication
{
    /// <summary>
    /// 自定义认证中心Handler
    /// 通常我们会提供一个统一的认证中心,负责证书的颁发及销毁(登入登出),而其他服务只用来验证证书,并用不到SingIn/SingOut
    /// </summary>
    public class MyAuthenticationHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
    {
        
        public AuthenticationScheme Scheme { get; private set; }
        protected HttpContext Context { get; private set; }
        //初始化上下文,Scheme
        public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
        {
            Scheme = scheme;
            Context = context;
            return Task.CompletedTask;
        }
        /// <summary>
        /// 登陆状态保存:一般用在登陆的时候,在判断用户名和密码正确了的情况下,调用这个方法
        /// 根据用户的Claims证件信息生成一个ticket票据,然后将这个票据保存到Cookie中去,这样浏览器就可以得到这个Cookie
        /// </summary>
        /// <param name="user"></param>
        /// <param name="properties"></param>
        /// <returns></returns>
        public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
        {
            //根据身份证信息,生成票据
            var ticket = new AuthenticationTicket(user, properties,Scheme.Name);
            Context.Response.Cookies.Append("myCookie", Serialize(ticket)); //将票据加入到Cookie中
            return Task.CompletedTask;
        }
        /// <summary>
        /// 登出:一般是删除这个登录Cookie
        /// </summary>
        /// <param name="properties"></param>
        /// <returns></returns>
        public Task SignOutAsync(AuthenticationProperties properties)
        {
            Context.Response.Cookies.Delete("myCookie");
            return Task.CompletedTask;
        }
        /// <summary>
        /// 在 AuthenticateAsync 中从Cookie中读取,如果能得到这个Cookie则表示用户已经登陆过了。
        /// </summary>
        /// <returns></returns>
        public Task<AuthenticateResult> AuthenticateAsync()
        {
            var cookie = Context.Request.Cookies["myCookie"];
            if (string.IsNullOrEmpty(cookie))
            {                            
                return Task.FromResult(AuthenticateResult.NoResult());//认证失败返回   
            }            
            return Task.FromResult(AuthenticateResult.Success(this.Deserialize(cookie))); //认证成功返回
        }
        //未登录:跳转到登陆页
        public Task ChallengeAsync(AuthenticationProperties properties)
        {
            Context.Response.Redirect("/Account/Login");
            return Task.CompletedTask;
        }
        //禁止访问:权限不够
        public Task ForbidAsync(AuthenticationProperties properties)
        {
            Context.Response.StatusCode = 403;
            return Task.CompletedTask;
        }

        //反序列化
        public AuthenticationTicket Deserialize(string content)
        {
            byte[] byteTicket = System.Text.Encoding.Default.GetBytes(content);
            return TicketSerializer.Default.Deserialize(byteTicket);
        }
        //序列化
        public string Serialize(AuthenticationTicket ticket)
        {
            byte[] byteTicket = TicketSerializer.Default.Serialize(ticket);
            return System.Text.Encoding.Default.GetString(byteTicket);
        }


    }
}

看看:Login.cshtml


@{
    ViewData["Title"] = "Login";
}
<h1>Login</h1>

@{
    <form asp-antiforgery="true" asp-controller="Account" asp-action="Register">
        User name: <input name="username" type="text" />
        Password: <input name="password" type="password"  value="123456"/>
        <input name="submit" value="Login" type="submit" />
        <input type="hidden" name="returnUrl" value="@TempData["returnUrl"]" />
    </form>
    
}

AccessDenied.cshtml


@{
    ViewData["Title"] = "AccessDenied";
}

<h1>大哥您没权限访问啊</h1>

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值