架構概覽
- 前端:HTML 表單 + jQuery 驗證 + `$.post` 非同步送出 + `return false` 阻止預設提交
- 後端:獨立頁面/ASHX/Web API 接收 `Request.Form`、驗證帳密、寫入 Session、回傳純文字/JSON 狀態碼
- 優點:無整頁重整、使用者體驗佳、前後端分離雛形
一、前端頁面:Login.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="Demo.Login" %>
<!DOCTYPE html>
<html><head><title>AJAX 登入</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
.input-error { border-color: #dc3545; }
.msg { margin-top:10px; padding:8px; border-radius:4px; display:none; }
.msg.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
.msg.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
</style>
</head><body>
<div style="max-width:360px; margin:50px auto; padding:20px; border:1px solid #ddd; border-radius:8px;">
<h3 class="text-center">會員登入</h3>
<div class="form-group">
<label>帳號</label>
<input type="text" id="txtUserName" class="form-control" placeholder="請輸入帳號" autocomplete="username" />
<span class="text-danger" id="valUserName"></span>
</div>
<div class="form-group">
<label>密碼</label>
<input type="password" id="txtPassword" class="form-control" placeholder="請輸入密碼" autocomplete="current-password" />
&span class="text-danger" id="valPassword"></span>
</div>
<button type="button" id="btnLogin" class="btn btn-primary btn-block">登入</button>
<div id="msgBox" class="msg"></div>
</div>
<script>
$(function () {
$('#btnLogin').on('click', function () {
// 1. 前端驗證
var user = $('#txtUserName').val().trim();
var pwd = $('#txtPassword').val();
var valid = true;
$('#valUserName, #valPassword').text('');
$('#txtUserName, #txtPassword').removeClass('input-error');
if (!user) { $('#valUserName').text('請輸入帳號'); $('#txtUserName').addClass('input-error'); valid = false; }
if (!pwd) { $('#valPassword').text('請輸入密碼'); $('#txtPassword').addClass('input-error'); valid = false; }
if (!valid) return;
// 2. 發送 AJAX
var $btn = $(this).prop('disabled', true).text('登入中...');
$('#msgBox').hide().removeClass('success error').text('');
$.post('LoginHandler.ashx', { UserName: user, UserPass: pwd }, function (res) {
$btn.prop('disabled', false).text('登入');
var msg = $('#msgBox').show();
switch (res) {
case '1': // 成功
msg.addClass('success').text('登入成功,正在跳轉...');
setTimeout(function () { window.location.href = 'Default.aspx'; }, 800);
break;
case '2': // 密碼錯誤
msg.addClass('error').text('密碼錯誤!');
$('#txtPassword').addClass('input-error').focus();
break;
case '3': // 帳號不存在
msg.addClass('error').text('帳號不存在!');
$('#txtUserName').addClass('input-error').focus();
break;
default:
msg.addClass('error').text('登入失敗:' + res);
}
}).fail(function () {
$btn.prop('disabled', false).text('登入');
$('#msgBox').show().addClass('error').text('網路或伺服器錯誤,請稍後再試');
});
});
});
</script>
</body></html>
二、後端處理程序:LoginHandler.ashx
using System;
using System.Web;
namespace Demo
{
public class LoginHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.CacheControl = "no-cache";
string userName = context.Request.Form["UserName"];
string userPass = context.Request.Form["UserPass"];
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(userPass))
{
context.Response.Write("0"); // 參數缺失
return;
}
// 模擬資料庫驗證(實際呼叫 Service/Repository)
var user = UserService.ValidateLogin(userName, userPass);
if (user != null)
{
// 登入成功:寫入 Session
context.Session["CurrentUser"] = user;
// 若需跨應用程式單一登入,這裡可簽發 FormsAuthentication Ticket
// FormsAuthentication.SetAuthCookie(userName, false);
context.Response.Write("1");
}
else
{
// 區分帳號不存在 vs 密碼錯誤(防止帳號掃描,實務建議統一提示「帳號或密碼錯誤」)
bool userExists = UserService.Exists(userName);
context.Response.Write(userExists ? "2" : "3");
}
}
public bool IsReusable => false;
}
// 簡易服務類別(實際應分層)
public static class UserService
{
public static UserModel ValidateLogin(string name, string pwd)
{
// 參數化查詢:SELECT * FROM Users WHERE UserName=@name AND PasswordHash=@hash
// 這裡模擬
if (name == "admin" && pwd == "123456") return new UserModel { Id = 1, Name = "admin", Role = "Admin" };
return null;
}
public static bool Exists(string name) => name == "admin"; // 模擬
}
public class UserModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Role { get; set; }
}
}
三、關鍵細節總結
| 項目 | 重點 |
| 按鈕型別 | `type="button"` + `return false` / `e.preventDefault()` 防止表單預設提交 |
| 前端驗證 | 必填、格式、長度,**後端必須再次驗證** |
| 傳輸加密 | **生產環境強制 HTTPS**,密碼不可明文傳輸 |
| 密碼處理 | 資料庫儲存 **BCrypt/Argon2/PBKDF2 雜湊**,不可明文/MD5/SHA1 |
| 防暴力破解 | 失敗計數、驗證碼、帳號鎖定、IP 限流 |
| 回傳格式 | 建議統一 JSON:`{ success: true, message: '', redirect: '' }` |
| CSRF 防護 | 單頁應用可用 AntiForgeryToken / SameSite Cookie / 雙重提交 Cookie |
| Session 固定攻擊 | 登入成功後 `Session.RegenerateID()` / 棄用舊 Session |
現代化改寫建議
// ASP.NET Core / Web API 版本
[HttpPost("login")]
public async Task Login([FromBody] LoginDto dto)
{
var user = await _userService.ValidateAsync(dto.UserName, dto.Password);
if (user == null) return Unauthorized(new { code = 401, message = "帳號或密碼錯誤" });
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Role, user.Role) };
var token = _jwtService.GenerateToken(claims);
return Ok(new { code = 200, token, user = new { user.Id, user.Name, user.Role } });
}
// 前端 fetch
const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userName, password }) });
const data = await res.json();
if (data.code === 200) { localStorage.setItem('token', data.token); router.push('/'); }