登入表單加密
登入表單有帳號密碼卻沒有加密。給予加密。
Last updated
登入表單有帳號密碼卻沒有加密。給予加密。
Last updated
using Newtonsoft.Json;
using NLog;
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
namespace YourProject.Controllers
{
public class AccountController : BaseController
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
[HttpGet]
public ActionResult Login()
{
//例外處理:應該先登出才能再登入。
if (Request.IsAuthenticated)
{
_logger.Info($"[{User.Identity.Name}] force logout.");
// clear session & cookie
Response.Cookies.Clear();
Session.Clear();
Session.Abandon();
Session.RemoveAll();
// sign out
FormsAuthentication.SignOut();
//# 重新轉址登入畫面
return RedirectToAction("Login", "Account");
}
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string ReturnUrl)
{
try
{
//## 解密 model.Credential;
var json = DecryptCredential(model.Credential);
model = JsonConvert.DeserializeObject<LoginModel>(json);
_logger.Debug($"[{model.UserId}] Login beginning.");
//## Authenticate
if (!AuthModule.Authenticate(model.UserId, model.Password))
{
//return new HttpUnauthorizedResult();
model.ErrorMessage = $"登入失敗!請確認帳號密碼是否正確。({DateTime.Now:HH:mm:ss})";
_logger.Error($"[{model.UserId}] Login => {model.ErrorMessage}");
return View(model);
}
//## 計簡登入識別欄位
bool isPersistent = (model.RememberMe == "remember"); // 記住我/永續性
//## 生成 auth-context JWT
var authCtx = new AuthContextModel
{
AuthUuid = Guid.NewGuid(),
UserId = model.UserId,
IssueDate = DateTime.Now,
ExpiresDate = DateTime.Now.Add(FormsAuthentication.Timeout),
};
var authUser = AuthModule.Authorize(authCtx);
string authContextJWT = AuthModule.MakeAuthContextJwt(authCtx);
//var decode = AuthModule.DecodeAuthContextJwt(authContextJWT);
string authTokenJWT = AuthModule.MakeAuthTokenJwt(authCtx, authContextJWT, authUser);
#region ## Make AuthCookie
/// 將使得 Request.IsAuthenticated == true ;
/// 將使得 [Authorize] 有作用
//# auth-ticket
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
2,
authCtx.UserId,
authCtx.IssueDate,
authCtx.ExpiresDate, // ticket 到期日
isPersistent, //永續性
authCtx.AuthUuid.ToString(), // 加入客制化資料:登入UUID
FormsAuthentication.FormsCookiePath
);
//# 寫入 auth-cookie
string encTicket = FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)
{
HttpOnly = true,
Secure = Request.IsSecureConnection,
SameSite = SameSiteMode.Strict,
Expires = isPersistent ? authCtx.ExpiresDate : DateTime.MinValue, // cookie 到期日
});
#endregion
//## 登入成功後 => 自動轉址並把 AuthContext 存入 localStorage。
_logger.Info($"[{authCtx.UserId}] Login success.");
model.ErrorMessage = "SUCCESS";
model.ReturnUrl = ReturnUrl;
model.AuthToken = EncryptToken(authTokenJWT);
return View(model);
}
catch(Exception ex)
{
_logger.Error($"[{model.UserId}] Login => Exception! {ex.Message}");
model.ErrorMessage = "401 Unauthorized";
model.ReturnUrl = null;
model.AuthToken = null;
return View(model);
}
}
[HttpGet]
[Authorize]
public ActionResult Logout()
{
_logger.Info($"[{User.Identity.Name}] logout.");
// clear session & cookie
Response.Cookies.Clear();
Session.Clear();
Session.Abandon();
Session.RemoveAll();
// sign out
FormsAuthentication.SignOut();
// 回首頁 302
return Redirect("~/");
}
}
public class LoginModel
{
public string UserId { get; set; } // 謹用於繫結UI而已。
public string Password { get; set; } // 謹用於繫結UI而已。
public string RememberMe { get; set; } // 謹用於繫結UI而已。
/// <summary>
/// [登入參數]登入認證憑證
/// </summary>
public string Credential { get; set; }
public string ErrorMessage { get; set; }
/// <summary>
/// [登入成功後]繫結登入者資訊。JWT 格式。
/// </summary>
public string AuthToken { get; set; }
/// <summary>
/// [登入成功後]轉址標的
/// </summary>
public string ReturnUrl { get; set; }
}
}@using System.Configuration;
@model YourProject.Controllers.LoginModel
@{
Layout = null;
//## 手動生成 AntiForgery。取代 @Html.AntiForgeryToken()。
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
Response.Cookies.Add(new HttpCookie("__RequestVerificationToken", cookieToken)
{
HttpOnly = true,
Secure = Request.IsSecureConnection,
});
}
<!DOCTYPE html>
<html>
<head>
...略...
</head>
<body>
@if (Model?.ErrorMessage == "SUCCESS")
{
// 登入成功
string redirectUrl = String.IsNullOrWhiteSpace(Model.ReturnUrl) ? "~/" : Model.ReturnUrl;
<script>
localStorage.setItem('auth-token', '@Model.AuthToken');
location.replace('@Url.Content(redirectUrl)');
</script>
}
else
{
// 登入輸入畫面
<div id="login-form"
data-at="@formToken"
data-ui="@(Model?.UserId)"
data-rm="@(Model?.RememberMe)"
data-em="@(Model?.ErrorMessage)" ></div>
<script src="~/Scripts/bundle/shared.bundle.js"></script>
<script src="~/Scripts/bundle/runtime.bundle.js"></script>
<script src="~/Scripts/bundle/login.bundle.js"></script>
}
</body>
</html>import React from 'react'
import { createRoot } from 'react-dom/client'
import LoginForm from './LoginForm'
(function () {
const rootElement = document.getElementById('login-form');
if (rootElement) {
const attrs = {
requestVerificationToken: rootElement.dataset.at,
userId: rootElement.dataset.ui,
rememberMe: rootElement.dataset.rm,
errMsg: rootElement.dataset.em,
}
const root = createRoot(rootElement);
root.render(
<LoginForm {...attrs} />
);
} else {
console.error("Root element not found");
}
})();import React, { useMemo, useState } from 'react'
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import { Alert, Avatar, Box, Button, Checkbox, Container, CssBaseline, FormControlLabel, Grid, Link, TextField, Typography } from '@mui/material';
import { useEncrypt } from '../common/hooks/useTools';
export default function LoginForm(props: {
userId?: string
rememberMe?: string
errMsg?: string
requestVerificationToken?: string
}) {
const [isSubmited, setIsSubmited] = useState(false)
const encrypt = useEncrypt()
const actionUrl = useMemo(() => {
const urlParams = new URLSearchParams(window.location.search);
const returnUrl = urlParams.get('ReturnUrl');
return Boolean(returnUrl)
? `/Account/Login?ReturnUrl=${returnUrl}`
: '/Account/Login'
}, [])
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setIsSubmited(true); // 封鎖UI
// 從輸入表單取得填入的帳密。
const form = event.target as HTMLFormElement;
const userId = (form.elements.namedItem('userId') as HTMLInputElement).value;
const password = (form.elements.namedItem('password') as HTMLInputElement).value;
const rememberMeInput = (form.elements.namedItem('rememberMe') as HTMLInputElement)
const rememberMe = rememberMeInput.checked ? rememberMeInput.value : null
// 動態生成真正登入用的表單
const form2 = document.createElement('form');
form2.action = actionUrl;
form2.method = 'POST';
// 創建隱藏輸入來提交加密後的數據
const encryptedCredentialInput = document.createElement('input');
encryptedCredentialInput.type = 'hidden';
encryptedCredentialInput.name = 'credential';
encryptedCredentialInput.value = encrypt(JSON.stringify({ userId, password, rememberMe })); // 加密登入帳密。
const requestVerificationToken = document.createElement('input');
requestVerificationToken.type = 'hidden';
requestVerificationToken.name = '__RequestVerificationToken'; // for 防偽
requestVerificationToken.value = props.requestVerificationToken;
// 加入 credential
form2.appendChild(encryptedCredentialInput);
form2.appendChild(requestVerificationToken);
// 將 form 附加到容器
document.getElementById('form-container').appendChild(form2);
// 送出表單
form2.submit();
}
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box id='form-container'
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form action={actionUrl} method="post" onSubmit={handleSubmit} >
<TextField
margin="normal"
required
fullWidth
id="userId"
label="帳號"
name="userId"
defaultValue={props.userId ?? 'smart'}
autoComplete="userId"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="密碼"
type="password"
id="password"
autoComplete="current-password"
defaultValue={'asvt'}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" name="rememberMe" defaultChecked={"remember" === props.rememberMe} />}
label="Remember me"
/>
{/*<input name="__RequestVerificationToken" type="hidden" value={props.requestVerificationToken} />*/}
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
disabled={isSubmited}
>
登入
</Button>
{Boolean(props?.errMsg) &&
<Alert severity="error" sx={{ my: 1 }}>{props.errMsg}</Alert>
}
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</Box>
</Container>
)
}