ASP.net Core login using Cookie Authentication
前言
登入是每個網站幾乎必備功能,剛開始接觸ASP.net Core,此項必學
不過ASP.net Framework時期的FormsAuthentication幾項功能來到ASP.net Core有幾點變化↓
1.Web.config定義:登入成功要導向的網址、尚未登入(Unauthorized)時要導向的網址、登入逾期時間
2.登入帳號寫入Cookie方式的改變
文章複習:[ASP.net MVC] ASP.net MVC整合FormsAuthentication表單驗證登入 - 簡易範例程式碼
※話說,去年我在恆逸上課的ASP.net Core 2 進階班,登入機制學習的是 ASP.Net Identy,真是嚇死寶寶了~ 簡直跟2005年 Asp.net 2 的 Membership Provider一樣,會產生一堆有的沒的資料庫欄位,維護困難直接棄坑
實作
本文 ASP.net Core 2.1、3.0版~之後版本都適用,大部份差異只在Startup.cs不一樣,其餘Code都一模一樣
以前 FormsAuthentication在 Web.config 檔裡的設定,現在必須在Startup.cs處理
以下是 ASP.net Core 3.0 的 Startup.cs
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
/*引用以下命名空間*/
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace AspCoreLoginTest
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//從組態讀取登入逾時設定
double LoginExpireMinute = this.Configuration.GetValue<double>("LoginExpireMinute");
//註冊 CookieAuthentication,Scheme必填
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
//或許要從組態檔讀取,自己斟酌決定
option.LoginPath = new PathString("/Home/Login");//登入頁
option.LogoutPath = new PathString("/Home/Logout");//登出Action
//用戶頁面停留太久,登入逾期,或Controller的Action裡用戶登入時,也可以設定↓
option.ExpireTimeSpan = TimeSpan.FromMinutes(LoginExpireMinute);//沒給預設14天
//↓資安建議false,白箱弱掃軟體會要求cookie不能延展效期,這時設false變成絕對逾期時間
//↓如果你的客戶反應明明一直在使用系統卻容易被自動登出的話,你再設為true(然後弱掃policy請客戶略過此項檢查)
option.SlidingExpiration = false;
});
services.AddControllersWithViews(options=> {
//↓和CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller就不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();//這樣的話,Controller、Action不必再加上[RequireHttps]屬性
app.UseStaticFiles();
app.UseRouting();
//留意寫Code順序,先執行驗證...
app.UseAuthentication();
app.UseAuthorization();//Controller、Action才能加上 [Authorize] 屬性
//微軟建議 ASP.net Core 3.0 開始改用Endpoint
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
以下是 ASP.net Core 2.1 的Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Mvc;
namespace AspCoreLoginTest
{
public class Startup
{
private readonly IConfiguration config;
public Startup(IConfiguration config)
{
this.config = config;
}
public void ConfigureServices(IServiceCollection services)
{
//從組態讀取登入逾時設定
double loginExpireMinute = this.config.GetValue<double>("LoginExpireMinute");
//註冊 CookieAuthentication,Scheme必填
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option=>
{
//或許要從組態檔讀取,自己斟酌決定
option.LoginPath = new PathString("/Home/Login");//登入頁
option.LogoutPath = new PathString("/Home/Logout");//登出Action
//用戶頁面停留太久,登入逾期,或Controller中用戶登入時也可設定
option.ExpireTimeSpan = TimeSpan.FromMinutes(loginExpireMinute );//沒給預設14天
//↓資安建議false,白箱弱掃軟體會要求cookie不能延展效期,這時設false變成絕對逾期時間
//↓如果你的客戶反應明明一直在使用系統卻容易被自動登出的話,你再設為true(然後弱掃policy請客戶略過此項檢查)
option.SlidingExpiration = false;
});
services.AddMvc(options => {
//↓和CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();//這樣的話,Controller、Action不必再加上[RequireHttps]屬性
app.UseStaticFiles();
//留意先執行驗證...
app.UseAuthentication();
//再執行Route,如此順序程式邏輯才正確
app.UseMvcWithDefaultRoute();
}
}
}
組態檔 appsettings.json 的設定,由於登入逾期時間,客戶時常改來改去,所以放在組態檔
{
"LoginExpireMinute": 60
}
登入頁、及處理登入
※微軟官網在呼叫SignInAsync()、SignOutAsync()時,有填寫CookieAuthenticationDefaults.AuthenticationScheme,但我自己實測可以省略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace AspCoreLoginTest.Controllers
{
//[AutoValidateAntiforgeryToken]//此項跟資安有關,只要是http method post,都要驗證token。Startup.cs有設定過全域驗證的話,這行可省略
public class HomeController : Controller
{
/// <summary>
/// 讀取組態用
/// </summary>
private readonly IConfiguration config;
public HomeController(IConfiguration config)
{
this.config = config;
}
/// <summary>
/// 登入頁
/// </summary>
/// <returns></returns>
public IActionResult Login()
{
return View();
}
/// <summary>
/// 表單post提交,準備登入
/// </summary>
/// <param name="form"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login(string Account, string pd,string ReturnUrl)
{//未登入者想進入必須登入的頁面,他會被自動導頁至/Home/Login,網址後面也會自動帶上名為ReturnUrl(原始要求網址)的QueryString
//pd是密碼,記得加密pd變數或雜湊過後再和DB資料比對
//從自己DB檢查帳&密,輸入是否正確
if ((Account == "shadow" && pd=="shadow")==false)
{
//帳&密不正確
ViewBag.errMsg = "帳號或密碼輸入錯誤";
return View();//流程不往下執行
}
//帳密都輸入正確,ASP.net Core要多寫三行程式碼
Claim[] claims = new[] { new Claim( "Account",Account) }; //Key取名"Account",在登入後的頁面,讀取登入者的帳號會用得到,自己先記在大腦
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims , CookieAuthenticationDefaults.AuthenticationScheme);//Scheme必填
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
//從組態讀取登入逾時設定
double loginExpireMinute = this.config.GetValue<double>("LoginExpireMinute");
//執行登入,相當於以前的FormsAuthentication.SetAuthCookie()
await HttpContext.SignInAsync(principal,
new AuthenticationProperties() {
IsPersistent = false, //IsPersistent = false:瀏覽器關閉立馬登出;IsPersistent = true 就變成常見的Remember Me功能
//用戶頁面停留太久,逾期時間,在此設定的話會覆蓋Startup.cs裡的逾期設定
/* ExpiresUtc = DateTime.UtcNow.AddMinutes(loginExpireMinute) */ });
//加上 Url.IsLocalUrl 防止Open Redirect漏洞
if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl) )
{
return Redirect(ReturnUrl);//導到原始要求網址
}
else
{
return RedirectToAction("ListData", "AfterLogin");//到登入後的第一頁,自行決定
}
}
/// <summary>
/// 登出
/// </summary>
/// <returns></returns>
//登出 Action 記得別加上[Authorize],不管用戶是否登入,都可以執行Logout
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToAction("Login", "Home");//導至登入頁
}
}
}
登入後的頁面
※如果你在Controller級別加上[Authorize] Filter ,但底下某些Action又想允許使用者未登入存取的話,未登入可存取的Action,一樣掛上 [AllowAnonymous] 這個Attribute
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AspCoreLoginTest.Controllers
{
//[AutoValidateAntiforgeryToken]//此項跟資安有關,只要是Http Method Post,都要驗證token。Startup.cs有設定過全域驗證的話,這行可省略
//↓避免Cross Site History Manipulation漏洞(用戶登出後,瀏覽器回上一頁仍可看到網頁的資安漏洞)
[ResponseCache(NoStore =true)]
public class AfterLoginController : Controller
{
//[Authorize]要加在Controller或Acton自行決定,有加上[Authorize]表示用戶必須事先登入才能瀏覽,用戶未登入就進來的話會被自動導頁
[Authorize]
public IActionResult ListData()
{
//可以使用 HttpContext.User.Identity.IsAuthenticated 來判斷用戶是否登入
//但由於此Action已經加上[Authorize],表示會執行此Action內容的一定都是登入者,所以不必再脫褲子放屁多寫判斷XD
StringBuilder sb = new StringBuilder();
sb.AppendLine("<ul>");
foreach (Claim claim in HttpContext.User.Claims)
{
sb.AppendLine($@"<li> claim.Type:{claim.Type} , claim.Value:{ claim.Value}</li>");
}
sb.AppendLine("</ul>");
ViewBag.msg = sb.ToString();//顯示用戶儲存在Cookie的資訊,本範例只有帳號,因為通常帳號不能更動(有的系統還拿來當DB Primary Key)
//若想取得其他用戶資料的話,請再自行使用此帳號去DB撈用戶資料
return View();
}
}
}
Login.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Login</title>
</head>
<body>
<!-- autocomplete="off" ,和 密碼欄位故意取名pd跟資安有關,那又是另一個議題-->
@using (Html.BeginForm("Login", "Home",new { ReturnUrl =Context.Request.Query["ReturnUrl"] }, FormMethod.Post, true, new { name = "myForm", autocomplete = "off" }))
{
<div>
<label>帳號:</label>@Html.TextBox("Account")
</div>
<div>
<label>密碼:</label>@Html.Password("pd")
</div>
<div>
<button type="submit">提交</button>
</div>
<div style="color:red;">
<!--顯示登入失敗訊息 -->
@ViewBag.errMsg
</div>
}
</body>
</html>
ListData.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ListData(登入後才能看)</title>
</head>
<body>
<div>
您的登入資訊↓
</div>
<div>
@Html.Raw(ViewBag.msg)
</div>
<div>
<a href="@Url.Action("Logout","Home")">登出</a>
</div>
</body>
</html>
執行結果 ↓
一開始如果用戶未登入想直接進入 /AfterLogin/ListData
會被導到 /Home/Login,注意QueryString 系統會自帶ReturnUrl,這點和以前的FormsAuthentication一樣
輸入帳密(shadow / shadow) ,登入成功畫面 ↓
如此看來,想取得用戶登入帳號的話 ↓
Controller.cs
HttpContext.User.Claims.FirstOrDefault(m=>m.Type=="Account").Value;
View (*.cshtml)
Context.User.Claims.FirstOrDefault(m=>m.Type=="Account").Value;
※ 2019-02-12追記:上述寫死字串 "Account" 似乎不是個好方法,微軟其實有內建類似列舉的字串可以使用,如下
登入時↓
//改使用ClaimTypes.Name就不必寫死字串, Account變數為用戶輸入的帳號
Claim[] claims = new[] { new Claim(ClaimTypes.Name, Account) };
取出帳號值↓
string Account = HttpContext.User.Claims.FirstOrDefault(m => m.Type == ClaimTypes.Name).Value;
※ 2019-01-29追記:原本我以為 jQuery Ajax遇上登入逾時,會像.Net MVC一樣後端執行導頁,前端Ajax callback function 會取得不正確結果 (請見:Handling session timeout in ajax calls)
一經嘗試,到了 ASP.net Core 變成用戶登入逾時(或未登入)執行Ajax,後端就只回傳Http Status Code 401、不執行導頁(ASP.net Core變聰明內建判斷是Ajax的話,後端就不執行導頁的樣子?)
如此一來,事情變得簡單許多,不必像.Net MVC一樣改寫AuthorizeAttribute
Ajax Request遇上登入逾時,事情都在前端jQuery處理即可
Sample Code↓
HomeController.cs
public IActionResult AjaxTest()
{
return View();
}
AjaxTest.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>AjaxTest</title>
</head>
<body>
<!--如果Ajax發出Http Post Method,有可能要加上這行,產生token(為了資安弱掃)-->
@using (Html.BeginForm("","",null,FormMethod.Post,true,new { name="myForm"}))
{
<button id="btnGo" type="button">點我發出Ajax</button>
}
<!--顯示Ajax呼叫成功結果-->
<div id="result">
</div>
<!--引用jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script type="text/javascript">
//寫在_Layout.cshtml 供其他頁面使用
//為所有的$.ajax呼叫設定預設值,當遇到StatusCode為401時,頁面導至登入頁
$.ajaxSetup({
statusCode: {
401: function () {//未授權,Unauthorized
//JS前端導頁
window.location.href = "@Url.Action("Login","Home")?ReturnUrl=" + encodeURIComponent(window.location.href);
}
}
});
</script>
<script type="text/javascript">
let AjaxUrl = "@Url.Action("PostData","AfterLogin")";
</script>
<script type="text/javascript">
$("#btnGo").on("click", function () {
$.ajax({
url: AjaxUrl,
headers: { RequestVerificationToken: $("input[name='__RequestVerificationToken'").val()}, //如果是發出Http Post Method,可能要加這行
method: "post",
success: function (result) {
$("#result").html(result);//畫面顯示結果
}
});
});
</script>
</body>
</html>
AfterLoginController.cs
[Authorize]//用戶登入才可存取
[HttpPost] //可能為了資安弱掃還要再加上 [AutoValidateAntiforgeryToken] 屬性 ,給看倌們自行決定XD
public IActionResult PostData()
{
return Content("Hello Ajax");
}
執行結果 ↓
用戶已登入的話,Ajax會取得後端回傳的文字
用戶未登入執行Ajax的話,畫面導頁至/Home/Login (QueryString記得手動寫Code帶上ReturnUrl)
結語
如果還有資安相關設定漏寫,日後有空補上
參考資源
登入
Asp.Net Core - simplest possible forms authentication by stackoverflow
微軟官網:使用沒有 ASP.NET Core 身分識別的 cookie 驗證 (Use cookie authentication without ASP.NET Core Identity)
ASP.NET CORE 2.0 COOKIE AUTHENTICATION、ASP.NET CORE中使用Cookie身份认证
ASP.net Core 1.x的同學請見:技術最前線 - ASP.NET Core 中的 Cookie、宣告與驗證
資安
[鐵人賽 Day28] ASP.NET Core 2 系列 - Response 快取
微軟官網:ASP.NET Core 中的回應快取
微軟官網:ASP.NET Core 中的防止跨網站要求偽造 (XSRF/CSRF) 攻擊
Automatically validating anti-forgery tokens in ASP.NET Core with the AutoValidateAntiforgeryTokenAttribute
↑ 翻譯:AutoValidateAntiforgeryToken加在Controller級別,專門對底下所有HttpPost的Action驗證token,才不用一個一個Action加上 [ValidateAntiForgeryToken]
Core 2 - Ajax submit with ValidateAntiForgeryToken not working
↑ 翻譯:由於加上filter [AutoValidateAntiforgeryToken],jQuery Ajax該如何送出AntiforgeryToken
在 ASP.net MVC 時期,從jQuery Ajax要發送AntiforgeryToken給後端要寫在 data 參數,如下↓
$.ajax({
url: "",
method: "post",
data: {
__RequestVerificationToken: $("input[name='__RequestVerificationToken']").val()
},
success: function (msg) { }
});//end ajax
但到了ASP.net Core時期,發送AntiforgeryToken要寫在jQuery Ajax的 headers 參數