• Asp.Net Core混合使用cookie和JwtBearer认证方案


    自己有时捣鼓一些小型演示项目,服务端主要是提供Web Api功能。为了便于管理,需要在服务端加一些简单的MVC网页,用管理员身份登录,做一些简单的操作。

    因此需要实现一个功能,在一个Asp.Net Core网站里,MVC网页用cookie认证,Web Api用JwtBearer认证。虽然Identity Server 4可以实现多种认证方案,但是我觉得它太重了,想直接在网站内集成2种认证方案。在网上没有找到现成的DEMO,自己折腾了一段时间搞定了,所以记录一下。

    创建cookie认证方案的MVC网站

    新建Asp.Net Core MVC项目。无身份验证。无https方便调试。

     

    添加登录网页视图模型类LoginViewModel

    public class LoginViewModel
        {
            public string UserName { get; set; } = "";
    
            [DataType(DataType.Password)]
            public string Password { get; set; } = "";
        }

    给Home控制器增加登录和注销函数,登录的时候要创建用户身份标识。

            [HttpGet]
            public IActionResult Login(string returnUrl = "")
            {
                ViewData["ReturnUrl"] = returnUrl;
                return View();
            }
    
            [HttpPost, ActionName("Login")]
            public async Task<IActionResult> LoginPost(LoginViewModel model, string returnUrl = "")
            {
                ViewData["ReturnUrl"] = returnUrl;
                if (ModelState.IsValid)
                {
                    bool succee = (model.UserName == "admin") && (model.Password == "123");
    
                    if (succee)
                    {
                        //创建用户身份标识
                        var claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
                        claimsIdentity.AddClaims(new List<Claim>()
                        {
                            new Claim(ClaimTypes.Sid, model.UserName),
                            new Claim(ClaimTypes.Name, model.UserName),
                            new Claim(ClaimTypes.Role, "admin"),
                        });
    
                        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
    
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        ModelState.AddModelError(string.Empty, "帐号或者密码错误。");
                        return View(model);
                    }
                }
    
                return View(model);
            }
    
            public async Task<IActionResult> Logout()
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    
                return Redirect("/Home/Index");
            }

    新建一个登录网页Login.cshtml

    @model MixAuth.Models.LoginViewModel
    
    @{
        ViewData["Title"] = "登录";
    }
    
    <div class="row">
        <div class="col-xs-10 col-sm-8 col-md-6">
            <form asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
    
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="UserName"></label>
                    <input asp-for="UserName" class="form-control" placeholder="请输入用户名" />
                    <span asp-validation-for="UserName" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Password"></label>
                    <input asp-for="Password" class="form-control" placeholder="请输入密码" />
                    <span asp-validation-for="Password" class="text-danger"></span>
                </div>
    
                <button type="submit" class="btn btn-primary">登录</button>
    
            </form>
        </div>
    </div>
    
    @section Scripts {
        @await Html.PartialAsync("_ValidationScriptsPartial")
    }
    

      

    然后在Startup.cs增加cookie认证方案,并开启认证中间件。

    public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                    {
                        //认证失败,会自动跳转到这个地址
                        options.LoginPath = "/Home/Login";
                    });
    
                services.Configure<CookiePolicyOptions>(options =>
                {
                    // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                    //options.CheckConsentNeeded = context => true;
                    options.MinimumSameSitePolicy = SameSiteMode.None;
                });
    
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                }
    
                app.UseStaticFiles();
                //app.UseCookiePolicy();
    
                //开启认证中间件
                app.UseAuthentication();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }

    给Home控制器的About函数增加认证要求。

            [Authorize]
            public IActionResult About()

    把网站跑起来,点击关于,就会跳转到登录页面,登录通过后,会调回关于页面。

    给网页再增加显示用户登录状态的功能。修改\Views\Shared\_Layout.cshtml,增加一个分部视图

    <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
                        <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
                        <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
                    </ul>
                    @await Html.PartialAsync("_LoginPartial")
                </div>
    

      

    _LoginPartial.cshtml分部视图内容

    @if (User.Identity.IsAuthenticated)
    {
        <form asp-controller="Home" asp-action="Logout" method="post" class="navbar-right">
            <ul class="nav navbar-nav navbar-right">
                <li>
                    <a href="#">@User.Identity.Name</a>
                </li>
                <li>
                    <button type="submit" class="btn btn-link navbar-btn navbar-link">
                        退出登录
                    </button>
                </li>
            </ul>
        </form>
    }
    else
    {
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-controller="Home" asp-action="Login" asp-route-returnUrl="/Home">
                    登录
                </a>
            </li>
        </ul>
    }
    

      

    现在可以点击页面导航栏的按钮的登录和注销了。

    至此,网页用cookie认证方案搞定。下面要在这个基础上,增加Web Api和JwtBearer认证。

    创建JwtBearer认证方案的Web Api控制器

    添加一个Web Api控制器,就用默认的value好了。

    增加一个JWTTokenOptions类,定义认证的一些属性。

    public class JWTTokenOptions
        {
            //谁颁发的
            public string Issuer { get; set; } = "server";
    
            //颁发给谁
            public string Audience { get; set; } = "client";
    
            //令牌密码
            public string SecurityKey { get; private set; } = "a secret that needs to be at least 16 characters long";
    
            //修改密码,重新创建数字签名
            public void SetSecurityKey(string value)
            {
                SecurityKey = value;
    
                CreateKey();
            }
    
            //对称秘钥
            public SymmetricSecurityKey Key { get; set; }
    
            //数字签名
            public SigningCredentials Credentials { get; set; }
    
            public JWTTokenOptions()
            {
                CreateKey();
            }
    
            private void CreateKey()
            {
                Key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey));
                Credentials = new SigningCredentials(Key, SecurityAlgorithms.HmacSha256);
            }
        }
    

      

    在startup.cs增加JwtBearer认证方案。

    public void ConfigureServices(IServiceCollection services)
            {
                JWTTokenOptions jwtTokenOptions = new JWTTokenOptions();
    
                services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                    {
                        //认证失败,会自动跳转到这个地址
                        options.LoginPath = "/Home/Login";
                    })
                    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
                    {
                        jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuerSigningKey = true,
                            IssuerSigningKey = jwtTokenOptions.Key,
    
                            ValidateIssuer = true,
                            ValidIssuer = jwtTokenOptions.Issuer,
    
                            ValidateAudience = true,
                            ValidAudience = jwtTokenOptions.Audience,
    
                            ValidateLifetime = true,
                            ClockSkew = TimeSpan.FromMinutes(5)
                        };
                    });
    
                services.Configure<CookiePolicyOptions>(options =>
                {
                    // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                    //options.CheckConsentNeeded = context => true;
                    options.MinimumSameSitePolicy = SameSiteMode.None;
                });
    
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }

    给value控制器增加认证方案,注意指定方案名称为JwtBearerDefaults.AuthenticationScheme。MVC控制器无需指定方案名称,因为默认就是CookieAuthenticationDefaults.AuthenticationScheme。

        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        [Route("api/[controller]")]
        [ApiController]
        public class ValueController : ControllerBase

    此时通过浏览器访问Web Api控制器,http://localhost:5000/api/Value,会得到401错误,这是对的,我们也不打算通过浏览器的方式访问Web Api,而是通过PC或者手机客户端。为了让认证客户端,需要增加一个获取Token的函数,暂时放在Home控制器,它的属性设置为[AllowAnonymous],允许未认证者访问。

            [AllowAnonymous]
            [HttpGet]
            public string GetToken(string userName, string password)
            {
                bool success = ((userName == "user") && (password == "111"));
                if (!success)
                    return "";
    
                JWTTokenOptions jwtTokenOptions = new JWTTokenOptions();
    
                //创建用户身份标识
                var claims = new Claim[] 
                {
                    new Claim(ClaimTypes.Sid, userName),
                    new Claim(ClaimTypes.Name, userName),
                    new Claim(ClaimTypes.Role, "user"),
                };
    
                //创建令牌
                var token = new JwtSecurityToken(
                    issuer: jwtTokenOptions.Issuer,
                    audience: jwtTokenOptions.Audience,
                    claims: claims,
                    notBefore: DateTime.Now,
                    expires: DateTime.Now.AddDays(1),
                    signingCredentials: jwtTokenOptions.Credentials
                    );
    
                string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
    
                return jwtToken;
            }

    编写客户端使用JwtBearer认证

    编写一个WPF客户端软件去获取Token,访问Web Api。

    private async Task GetTokenAsync()
            {
                try
                {
                    using (WebClient client = new WebClient())
                    {
                        //地址
                        string path = $"{webUrl}/Home/GetToken?userName=user&password=111";
    
                        token = await client.DownloadStringTaskAsync(path);
    
                        txbMsg.Text = $"获取到令牌={token}";
                    }
                }
                catch (Exception ex)
                {
                    txbMsg.Text = $"获取令牌出错={ex.Message}";
                }
            }
    
            private async Task GetValueAsync()
            {
                try
                {
                    using (WebClient client = new WebClient())
                    {
                        //地址
                        string path = $"{webUrl}/api/Value";
    
                        client.Headers.Add(HttpRequestHeader.Authorization, $"Bearer {token}");
    
                        string value = await client.DownloadStringTaskAsync(path);
    
                        txbMsg.Text = $"获取到数据={value}";
                    }
                }
                catch (Exception ex)
                {
                    txbMsg.Text = $"获取数据出错={ex.Message}";
                }
            }

    如果直接获取数据,能够捕捉到401错误。

    先获取令牌。

    再获取数据,就没问题了。

    DEMO代码参见:

    https://github.com/woodsun2018/MixAuth

  • 相关阅读:
    Java中的String问题
    【转载】Java与C++语言在作用域上的差异浅析
    【转载】Java是传值还是传引用
    Boost学习资源
    程序4-4 chmod函数实例
    程序4-3 umask函数实例
    程序4-2 access函数实例
    程序4-1 对每个命令行参数打印文件类型
    程序3-5 对一个文件描述符打开一个或多个文件状态标志
    程序3-4 对指定的描述符打印文件标志
  • 原文地址:https://www.cnblogs.com/sunnytrudeau/p/9693512.html
Copyright © 2020-2023  润新知