(轉)Asp.Net Core混合使用cookie和JwtBearer認證方案

轉:https://www.cnblogs.com/sunnytrudeau/p/9693512.html

一些小型演示項目,服務端主要是提供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"),
                    });
            //調用HttpContext.SignInAsync進行登錄,注意此方法的第一個參數,必需與StartUp.cs中services.AddAuthentication的參數相同,
//AddAuthentication是設置登錄,SigninAsync是按設置參數進行登錄 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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,增加一個分部視圖

1
2
3
4
5
6
7
8
<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分部視圖內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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類,定義認證的一些屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class JWTTokenOptions
    {
        //誰頒發的
        public string Issuer { getset; } = "server";
 
        //頒發給誰
        public string Audience { getset; } = "client";
 
        //令牌密碼
        public string SecurityKey { getprivate set; } = "a secret that needs to be at least 16 characters long";
 
        //修改密碼,重新創建數字簽名
        public void SetSecurityKey(string value)
        {
            SecurityKey = value;
 
            CreateKey();
        }
 
        //對稱祕鑰
        public SymmetricSecurityKey Key { getset; }
 
        //數字簽名
        public SigningCredentials Credentials { getset; }
 
        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 =>
                {
                    //認證失敗,會自動跳轉到這個地址(認證使用[Authorize]屬性)
                    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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章