.NetFramework
Login
ASP.NET Identity
.Net Framework 會員登入有兩種,一種是 ASP.NET Identity ,另一種是Form Authentication。
建立MVC專案時,驗證選項可以選"個別使用者帳戶",建立時自動產生Identity會員登入相關程式碼,或是將 ASP.NET Identity相關程式碼新增至Empty專案。
首先新增Empty MVC專案(無驗證)。如果你有現有的專案也可以。
在HomeController加上[Authorize]
標籤
進到首頁會得到HTTP Error 401.0 – Unauthorized回應
接著在專案下 Web.config 新增authentication設定
<configuration>
...
<system.web>
...
<authentication mode="Forms">
<forms loginUrl="/Account/Login" timeout="2880" defaultUrl="/Home/Index" />
</authentication>
</system.web>
...
</configuration>
由於登入路徑設為/Account/Login,所以我們要建立相對應的Controller, Action與View
重新將網站run起來就會導至設定的登入頁面
當使用者成功登入時(此部分省略),要新增驗證Cookie
var ticket = new FormsAuthenticationTicket(
version: 1,
name: account, //可以放使用者Id
issueDate: DateTime.UtcNow,//現在UTC時間
expiration: DateTime.UtcNow.AddMinutes(30),//Cookie有效時間=現在時間往後+30分鐘
isPersistent: true,// 是否要記住我 true or false
userData: string.Join(",", roles), //可以放使用者角色名稱
cookiePath: FormsAuthentication.FormsCookiePath
);
var encryptedTicket = FormsAuthentication.Encrypt(ticket); //把驗證的表單加密
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
Response.Cookies.Add(cookie);
以上就是From Authentication主要的內容,不含帳號註冊驗證等步驟,詳細步驟見下方Login and Register steps。
接下來主要的內容就是實作登入的功能以及相關流程。
因為需要用到資料庫,可以先參考code first方法建立資料庫。
需要使用到的User資料表
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
public int PasswordWorkFactor { get; set; }
}
先在Models/ViewModes/資料夾下新增LoginViewModel.cs與RegisterViewModel.cs檔案給登入和註冊頁面使用,
public class LoginViewModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
public class RegisterViewModel
{
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string PasswordConfirmed { get; set; }
}
新增AccountController.cs,先有個大致上的樣子稍後會再修改。
public class AccountController : Controller
{
private MyAppContext context = new MyAppContext();
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
return View();
}
[HttpGet]
public ActionResult Register()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
return View();
}
public ActionResult Logout(){
}
}
簡單調整一下Site.css
body {
background: linear-gradient(135deg, #8e13e3, #4e3cc1);
min-height: 100vh;
}
_Layout.cshtml連結部分修改一下加上登入與登出連結,原本navbar-nav
後加上mr-auto
,並再加上新的ul.navbar-nav
,並加上User.Identity.IsAuthenticated
判斷是否登入。
<ul class="navbar-nav mr-auto">
<li class="nav-item active">@Html.ActionLink("首頁", "Index", "Home", null, new { @class = "nav-link" })</li>
<li class="nav-item">@Html.ActionLink("關於", "About", "Home", null, new { @class = "nav-link" })</li>
<li class="nav-item">@Html.ActionLink("連絡人", "Contact", "Home", null, new { @class = "nav-link" })</li>
</ul>
<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
<li class="nav-item">@Html.ActionLink("登出", "Logout", "Account", null, new { @class = "nav-link" })</li>
}
else
{
<li class="nav-item">@Html.ActionLink("登入", "Login", "Account", null, new { @class = "nav-link" })</li>
}
</ul>
加入Login.cshtml頁面,這邊Forgot password僅示意不實做。
<div class="card align-items-center shadow mx-auto my-5 px-4 py-5" style="width: 350px;">
<h2>Sign in</h2>
<form action="/Account/Login" method="post" class="w-100">
@Html.AntiForgeryToken()
<div class="form-group">
<label for="Email">Email address</label>
<input type="email" class="form-control" id="Email" name="Email" placeholder="example@example.com" data-val="true" data-val-required="required" data-val-email="email only">
<span class="field-validation-valid text-danger" data-valmsg-for="Email" data-valmsg-replace="true"></span>
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<div class="d-flex justify-content-between">
<label for="Password">Password</label><small><a href="#" class="text-muted">Forgot Password?</a></small>
</div>
<input type="password" class="form-control" id="Password" name="Password" data-val="true" data-val-required="required">
<span class="field-validation-valid text-danger" data-valmsg-for="Password" data-valmsg-replace="true"></span>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label text-muted" for="exampleCheck1">Remember me</label>
</div>
<button type="submit" class="btn btn-primary w-100">Sign in</button>
<small><a href="/Account/Register">Sign up here</a></small>
</form>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
加入Register.cshtml頁面,"I accept the Terms and Conditions"僅示意不實作。
<div class="card align-items-center shadow mx-auto my-5 px-4 py-5" style="width: 400px;">
<h2>Create your account</h2>
<form action="/Account/Register" method="post" class="w-100">
@Html.AntiForgeryToken()
<div class="form-group">
<label for="Name">Your name</label>
<input type="text" class="form-control" id="Name" name="Name" data-val="true" data-val-required="required">
<span class="field-validation-valid text-danger" data-valmsg-for="Name" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label for="Email">Email address</label>
<input type="email" class="form-control" id="Email" name="Email" placeholder="example@example.com" data-val="true" data-val-required="required" data-val-email="email only">
<span class="field-validation-valid text-danger" data-valmsg-for="Email" data-valmsg-replace="true"></span>
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="Password">Password</label>
<input type="password" class="form-control" id="Password" name="Password" data-val="true" data-val-required="required">
<span class="field-validation-valid text-danger" data-valmsg-for="Password" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label for="PasswordConfirmed">Password Confirmed <span class="field-validation-valid text-danger" data-valmsg-for="PasswordConfirmed" data-valmsg-replace="true"></span></label>
<input type="password" class="form-control" id="PasswordConfirmed" name="PasswordConfirmed" data-val="true" data-val-required="required" data-val-equalto="not match" data-val-equalto-other="Password">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label text-muted" for="exampleCheck1">
I accept the <a href="#">Terms and Conditions</a>
</label>
</div>
<button type="submit" class="btn btn-primary w-100">Create an account</button>
</form>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Login, Register view 都有加上 jquery.validate.unobtrusive驗證項目
接著要來修改一下Controller。
先在專案下新增Services資料夾和AccountService.cs檔案,再來直接使用IntelliSense幫你產生對應方法。
先從註冊開始,在Register Post方法加上註冊流程,成功註冊新增使用者後將頁面導至登入頁,不然停留在原註冊頁面;新增使用者詳細邏輯放在service內實作。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
AccountService service = new AccountService();
bool isCreated = service.CreateUser(viewModel);
if (isCreated)
{
return RedirectToAction("Login");
}
return View(viewModel);
}
再來是登入,在Login Post方法加上註冊流程,成功驗證使用者後產生並加入Cookie,導回首頁;詳細邏輯同樣放在service內實作。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
AccountService service = new AccountService();
bool isSuccess = service.ValidateUser(viewModel);
if (isSuccess)
{
HttpCookie cookie = service.GenCookie(viewModel.Email);
Response.Cookies.Add(cookie);
return RedirectToAction("Index", "Home");
}
return View(viewModel);
}
登出比較簡單就這樣而已。
public ActionResult Logout()
{
FormsAuthentication.SignOut();
Session.RemoveAll();
return RedirectToAction("Index","Home");
}
這時候AccountService應該會有三個未實作的方法CreateUser
, ValidateUser
, GenCookie
;在實作方法前先新增UserToken.cs類別,稍後會用到。
public class UserToken
{
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
public int PasswordWorkFactor { get; set; }
}
AccountService.cs新增兩個private方法,稍後CreateUser
, ValidateUser
使用。
private bool HasUser(string email)
{
return context.Users.Any(x => x.Email == email);
}
private User GetUser(string email)
{
return context.Users.First(x => x.Email == email);
}
先前由Controller註冊使用者時呼叫AccountService.CreateUser()方法,這裡主要產生驗證的東西與將使用者新增至資料庫。(EncryptService.cs稍後新增)
public bool CreateUser(RegisterViewModel viewModel)
{
if (!HasUser(viewModel.Email))
{
UserToken token = new EncryptService().GenerateUserToken(viewModel.Password);
User user = new User
{
Name = viewModel.Name,
Email = viewModel.Email,
PasswordHash = token.PasswordHash,
PasswordSalt = token.PasswordSalt,
PasswordWorkFactor = token.PasswordWorkFactor,
};
context.Users.Add(user);
context.SaveChanges();
return true;
}
return false;
}
由Controller登入時驗證該使用者
public bool ValidateUser(LoginViewModel viewModel)
{
if (HasUser(viewModel.Email))
{
User user = GetUser(viewModel.Email);
bool isValid = new EncryptService().IsValidPassword(user.PasswordHash, user.PasswordSalt, viewModel.Password);
return isValid;
}
return false;
}
成功驗證使用者後產生Cookie。
public HttpCookie GenCookie(string email)
{
var ticket = new FormsAuthenticationTicket(
version: 1,
name: email, //可以放使用者Id
issueDate: DateTime.UtcNow,//現在UTC時間
expiration: DateTime.UtcNow.AddMinutes(30),//Cookie有效時間=現在時間往後+30分鐘
isPersistent: true,// 是否要記住我 true or false
userData: "", //可以放使用者角色名稱
cookiePath: FormsAuthentication.FormsCookiePath
);
var encryptedTicket = FormsAuthentication.Encrypt(ticket); //把驗證的表單加密
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
{ // 重點!!避免 Cookie遭受攻擊、盜用或不當存取。請查詢關鍵字「」。
HttpOnly = true // 必須上網透過http才可以存取Cookie。不允許用戶端(寫前端程式)存取
//Secure = true; // 需要搭配https(SSL)才行。
};
return cookie;
}
接著就是做還沒做的EncryptService。我們一樣在Services資料夾內新增EncryptService.cs檔案,需要三個property。
public int SaltLength { get; private set; }
public int HashLength { get; private set; }
public int Iteration { get; private set; }
新增建構子多載,可供呼叫端自訂Salt, Hash長度與Work factor (可參考下方Salted Password Hashing內文章:Making Password Cracking Harder: Slow Hash Functions)
public EncryptService() : this(256, 256, 5)
{
}
public EncryptService(int saltLength, int hashLength, int iteration)
{
SaltLength = saltLength;
HashLength = hashLength;
Iteration = iteration;
}
我們主要需要的方法有:GenerateSalt
, GenerateHash
, 以及AccountService用到的GenerateUserToken
,IsValidPassword
GenerateSalt
, GenerateHash
只需要private就好,此部分參考Password Hash Example
private byte[] GenerateSalt(int length)
{
var bytes = new byte[length];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(bytes);
}
return bytes;
}
private byte[] GenerateHash(byte[] password, byte[] salt, int iterations, int length)
{
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, iterations))
{
return deriveBytes.GetBytes(length);
}
}
另外還需要轉Base64編碼方法,因使用者輸入密碼可能含有非Base64編碼內容,無法直接使用Convert.FromBase64String()
轉換。
private static string Base64Encode(string plainText)
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
剩下兩個AccountService用到的方法:GenerateUserToken
,IsValidPassword
GenerateUserToken為產生隨機salt再結合user輸入的密碼雜湊,將所需內容放入UserToken回傳。
public UserToken GenerateUserToken(string password)
{
var encodePass = Base64Encode(password);
var salt = GenerateSalt(SaltLength);
var hash = GenerateHash(Convert.FromBase64String(encodePass), salt, Iteration, HashLength);
UserToken token = new UserToken
{
PasswordSalt = Convert.ToBase64String(salt),
PasswordHash = Convert.ToBase64String(hash),
PasswordWorkFactor = Iteration
};
return token;
}
這邊驗證只驗證輸入的hash值是否與雜湊過的輸入密碼相等。
public bool IsValidPassword(string inputHash, string inputSalt, string password)
{
var encodePass = Base64Encode(password);
var hash = GenerateHash(Convert.FromBase64String(encodePass), Convert.FromBase64String(inputSalt), Iteration, HashLength);
var hasString = Convert.ToBase64String(hash);
return inputHash.Equals(hasString);
}
:::info 本篇主要以執行 .NET 5(.NETCore 5)為主。 ::: 先到NAS上的套件中心搜尋docker並安裝 安裝好後會多一個docker共享資料夾,稍後會使用到 Deploy .NET 5 in Docker on NAS 方法1 倉庫伺服器預設已經有Docker Hub
May 25, 2021如果還不清楚Windows Terminal參考下圖,可從Microsoft Store下載。 此篇記錄一下如何在Visual Studio 2019內設定外部工具開啟Windows Terminal,設定為上方選單工具->外部工具 點選開啟後會跳出以下視窗 這邊設定填入 :::info Windows Terminal位置可以先找一下,可能因版本差異有所不同。 :::
Oct 14, 2020ASP.NET MVC 範本專案建立後的Bootstrap版本預設為3.4.1版,view部分內容也是Bootstrap3的寫法。 首先直接更新Bootstrap(4.5.2),因為相依性這邊會一併安裝popper.js,並將glyphicon移除(icon使用可參考 https://icones.netlify.app/) :::warning 如果不需要將預設樣板改成Bootstrap 4,以下可以不用參考,直接更新nuget即可。 ::: 這時首頁會從
Oct 7, 2020用 GitHub Actions 將 .Net Framework web 自動部署至 Azure。 當時主要參考Calvin A. Allen的兩篇文章 Building .NET Framework Applications with Github Actions Git Tag Based Released Process Using GitHub Actions yml建置部署流程檔建立可由GitHub網站建立,或自行新增檔案。 由GitHub網站建立
Oct 6, 2020or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up