首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 圖庫權限控管實作:會員等級驗證、登入失敗鎖定、浮水印動態產生與泛型處理常式下載

     編輯:ASP.NET 基礎

專案情境

  • **一般會員** (Level=0):下載帶浮水印的試用圖片。
  • **付費會員** (Level=1):下載原始高畫質圖片。
  • **未登入/驗證失敗**:導向倒數跳轉頁,提示登入。
  • **登入防暴力**:錯誤 5 次鎖定 30 分鐘。

資料庫設計 (T_userInfo)

CREATE TABLE T_userInfo (
    ID INT IDENTITY(1,1) PRIMARY KEY,
    sUserName NVARCHAR(50) NOT NULL UNIQUE,
    sPassword NVARCHAR(100) NOT NULL, -- 實務請存雜湊值 (BCrypt/Argon2)
    iLevel TINYINT NOT NULL DEFAULT 0, -- 0=一般, 1=付費
    sErrorTime INT NULL,               -- 累積錯誤次數
    sLastErrorTime DATETIME NULL       -- 最後錯誤時間
);

登入頁面 (login.aspx / .cs)

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="login.aspx.cs" Inherits="PhotoGallery.Login" %>
<form id="form1" runat="server">
    <asp:Label Text="帳號:" AssociatedControlID="txtUser" />
    <asp:TextBox ID="txtUser" runat="server" />
    <br />
    <asp:Label Text="密碼:" AssociatedControlID="txtPwd" />
    <asp:TextBox ID="txtPwd" runat="server" TextMode="Password" />
    <br />
    <asp:Button ID="btnLogin" Text="登入" OnClick="btnLogin_Click" />
    <br />
    <asp:Label ID="lblMsg" runat="server" ForeColor="Red" Visible="false" />
</form>
using System;
using System.Web.UI;
using PhotoGallery.DAL; // TableAdapter 命名空間

namespace PhotoGallery
{
    public partial class Login : Page
    {
        protected void btnLogin_Click(object sender, EventArgs e)
        {
            var adapter = new T_userInfoTableAdapter();
            var dt = adapter.GetDataByUserName(txtUser.Text.Trim());

            if (dt.Count == 0)
            {
                ShowMsg("帳號不存在");
                return;
            }

            var user = dt[0]; // Strongly-typed DataRow

            // 檢查鎖定
            if (!user.IsNull("sErrorTime") && !user.IsNull("sLastErrorTime"))
            {
                double minutes = (DateTime.Now - user.sLastErrorTime).TotalMinutes;
                if (minutes <= 30 && user.sErrorTime >= 5)
                {
                    ShowMsg($"錯誤超過 5 次,請等待 {30 - (int)minutes} 分鐘後再試");
                    return;
                }
            }

            // 驗證密碼 (示範明文,實務請用 BCrypt.Verify)
            if (user.sPassword == txtPwd.Text)
            {
                Session["Auth"] = true;
                Session["UserId"] = user.ID;
                Session["UserLevel"] = user.iLevel;
                Session["UserName"] = user.sUserName;

                adapter.ResetErrorTime(user.ID); // 清零錯誤計數
                Response.Redirect("PicList.aspx");
            }
            else
            {
                adapter.IncErrorTime(user.ID); // 錯誤+1、更新時間
                ShowMsg("密碼錯誤");
            }
        }

        private void ShowMsg(string msg)
        {
            lblMsg.Text = msg;
            lblMsg.Visible = true;
        }
    }
}

圖片列表頁 (PicList.aspx)

<ul>
    <li><a href="PicDownload.ashx?f=photo1.jpg">風景照 1</a></li>
    <li><a href="PicDownload.ashx?f=photo2.jpg">人像照 2</a></li>
    <li><a href="PicDownload.ashx?f=photo3.jpg">微距照 3</a></li>
</ul>

下載處理常式 (PicDownload.ashx) — 核心權限與浮水印邏輯

<%@ WebHandler Language="C#" Class="PicDownload" %>
using System;
using System.Web;
using System.Web.SessionState;
using System.Drawing;
using System.Drawing.Imaging;
using PhotoGallery.DAL;

public class PicDownload : IHttpHandler, IRequiresSessionState
{
    public void ProcessRequest(HttpContext context)
    {
        // 1. 驗證登入
        if (context.Session["Auth"] == null || !(bool)context.Session["Auth"])
        {
            context.Response.Redirect("Target.htm"); // 倒數跳轉登入頁
            return;
        }

        string fileName = context.Request.QueryString["f"];
        if (string.IsNullOrEmpty(fileName) || fileName.Contains(".."))
        {
            context.Response.StatusCode = 400;
            return;
        }

        int userId = (int)context.Session["UserId"];
        int userLevel = (int)context.Session["UserLevel"];
        string userName = context.Session["UserName"]?.ToString() ?? "User";

        string physicalPath = context.Server.MapPath("~/Images/Original/" + fileName);
        if (!System.IO.File.Exists(physicalPath))
        {
            context.Response.StatusCode = 404;
            return;
        }

        // 2. 依等級決定輸出
        if (userLevel == 0) // 一般會員:加浮水印
        {
            context.Response.ContentType = "image/jpeg";
            string encodedName = HttpUtility.UrlEncode("Watermark_" + fileName);
            context.Response.AddHeader("Content-Disposition", $"attachment; filename*=UTF-8''{encodedName}");

            using (Bitmap src = new Bitmap(physicalPath))
            using (Graphics g = Graphics.FromImage(src))
            {
                string mark = $"試用版 - {userName} - {DateTime.Now:yyyy/MM/dd}";
                Font font = new Font("微軟正黑體", 20, FontStyle.Bold);
                Brush brush = new SolidBrush(Color.FromArgb(128, 255, 0, 0)); // 半透明紅
                // 右下角定位
                SizeF sz = g.MeasureString(mark, font);
                float x = src.Width - sz.Width - 20;
                float y = src.Height - sz.Height - 20;
                g.DrawString(mark, font, brush, x, y);

                src.Save(context.Response.OutputStream, ImageFormat.Jpeg);
            }
        }
        else // 付費會員:原檔直傳
        {
            context.Response.Clear();
            context.Response.ContentType = "image/jpeg";
            string encodedName = HttpUtility.UrlEncode(fileName);
            context.Response.AddHeader("Content-Disposition", $"attachment; filename*=UTF-8''{encodedName}");
            context.Response.WriteFile(physicalPath);
            context.Response.Flush();
            context.Response.End();
        }
    }
    public bool IsReusable => false;
}

倒數跳轉頁 (Target.htm)

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>導向中</title></head><body>
    請先登入,5 秒後自動導向登入頁。
    <a href="login.aspx">立即前往</a>
    <div id="countdown">5</div>
    <script>
        var s = 5;
        var t = setInterval(() => {
            document.getElementById('countdown').textContent = --s;
            if (s <= 0) { clearInterval(t); location.href = 'login.aspx'; }
        }, 1000);
    </script>
</body></html>

TableAdapter 關鍵方法 (DataSetPic.xsd 設計)

  • `GetDataByUserName(string userName)` → `SELECT * FROM T_userInfo WHERE sUserName = @userName`
  • `GetDataById(int id)` → `SELECT * FROM T_userInfo WHERE ID = @id`
  • `IncErrorTimeById(int id)` → `UPDATE T_userInfo SET sErrorTime = ISNULL(sErrorTime,0)+1, sLastErrorTime = GETDATE() WHERE ID = @id`
  • `ResetErrorTimeById(int id)` → `UPDATE T_userInfo SET sErrorTime = 0 WHERE ID = @id`

安全與效能補強建議

  1. 密碼雜湊:註冊/變更密碼時存 `BCrypt.HashPassword(pwd)`,登入驗證用 `BCrypt.Verify(pwd, hash)`。
  2. SQL 參數化:TableAdapter 內建參數化,已防注入。
  3. 檔案路徑驗證:`Path.GetFileName()` + 白名單副檔名,防目錄遍歷。
  4. 浮水印效能:大量下載時考慮**預先產生浮水印版本**快取至磁碟/Redis,避免即時 `Graphics` 運算阻塞執行緒。
  5. HTTPS 強制:保護 Session Cookie、傳輸加密。
  6. 權限集中化:未來擴充角色/權限矩陣,建議抽離 `IAuthorizationService` 介面。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved