UWInfo Blog
發表新文章
[Join] | [忘記密碼] | [Login]
搜尋

搜尋意見
文章分類-#Author#
[所有文章分類]
所有文章分類
  • ASP.NET (48)
  • ASP.NET2.0 (15)
  • ASP.NET4.0 (34)
  • JavaScript (49)
  • jQuery (26)
  • FireFox (4)
  • UW系統設定 (3)
  • SQL (39)
  • SQL 2008 (25)
  • mirror (4)
  • SVN (4)
  • IE (9)
  • IIS (20)
  • IIS6 (1)
  • 閒聊 (7)
  • W3C (6)
  • 作業系統 (9)
  • C# (24)
  • CSS (12)
  • FileServer (1)
  • HTML 5 (11)
  • CKEditor (3)
  • UW.dll (13)
  • Visual Studio (16)
  • Browser (8)
  • SEO (1)
  • Google Apps (3)
  • 網站輔助系統 (4)
  • DNS (5)
  • SMTP (4)
  • 網管 (11)
  • 社群API (3)
  • SSL (4)
  • App_Inventor (1)
  • URLRewrite (2)
  • 開發工具 (6)
  • JSON (1)
  • Excel2007 (1)
  • 試題 (3)
  • LINQ (1)
  • bootstrap (0)
  • Vue (3)
  • IIS7 (3)
  • foodpanda (2)
  • 編碼 (2)
  • 資安 (3)
  • Sourcetree (1)
  • MAUI (1)
  • CMD (1)
  • my sql (1)
最新回應
  • Newtonsoft.Json.JsonConvert.DeserializeObject 失敗的情況
    test...more
  • dotnet ef dbcontext scaffold
    ...more
  • [ASP.NET] 利用 aspnet_regiis 加密 web.config
    ...more
  • IIS ARR (reverse proxy) 服務安裝
    ...more
  • [錯誤訊息] 請加入 ScriptResourceMapping 命名的 jquery (區分大小寫)
    ...more
  • 用 Javascript 跨網頁讀取 cookie (Cookie cross page, path of cookie)
    ...more
  • 線上客服 - MSN
    本人信箱被盜用以致資料外洩,是否可以請貴平台予以協助刪除該信箱之使用謝謝囉...more
  • 插入文字到游標或選取處
    aaaaa...more
  • IIS 配合 AD (Active Directory) 認証, 使用 .Net 6.0
    太感謝你了~~~你救了我被windows 認證卡了好幾天QQ...more
  • PostgreSQL 的 monitor trigger
    FOR EACH ROW 可能要改為 FOR EACH STATEMENT ...more
標籤
  • 8330
  • letsencryp
  • 8360
  • shop
  • editor
  • 合約
  • en21211211
  • 60
  • 驗證
  • Su
  • 不分大小寫
  • TCP
  • SES
  • if21211211
  • RequestEnc
  • max
  • vs
  • -9829 UNIO
  • 200
  • table
  • SQL
  • FireFox
  • iif
  • [U2]
  • 230
  • ef
  • IE
  • C
  • div
  • pear
  • core
  • config ord
  • 黑貓
  • 1
  • -1290
  • 18
  • 貨幣
  • 0
  • net2121121
  • q8VQO2HG
  • 978
  • hkscs
  • -8048 UNIO
  • GN
  • request.qu
  • jq
  • PG
  • 交易記錄已滿
  • 392
  • 2873
頁數 1 / 5 下一頁
搜尋 pg 結果:
postgreSql 大型 table 跑 Count 非常慢
可參考本文章:

https://dba.stackexchange.com/questions/245990/postgresql-extremely-slow-count-with-index-simple-query

vacuum analyze insider_event;
執行了 7 分鐘

ALTER TABLE insider_event SET (autovacuum_vacuum_scale_factor = 0, autovacuum_analyze_scale_factor = 0,  autovacuum_vacuum_threshold = 10000, autovacuum_analyze_threshold = 10000);

跑了 1.2 分鐘

修改 work_mem:
https://www.pgmustard.com/blog/work-mem

記憶體調整
https://www.enterprisedb.com/postgres-tutorials/how-tune-postgresql-memory
More...
Bike, 2024/2/5 上午 06:55:40
Restful 的 API 範例
Restful 的 API 範例,比較特別的是取得單一筆資料時,不是用一般常見的 {id} 而是用 get?id=xxx 的方式,以避免 XXS 的功擊。(不要把原網頁中的參數拼入 API 網址,要改用 Query String 的方式傳給 API)

using Ds;
using Ds.Gv;
using iText.Kernel.Geom;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NPOI.SS.Formula.Functions;
using NPOI.SS.Util;
using Su;
using System.Linq.Expressions;

namespace CallCampaign.Api
{
    /// <summary>
    /// 行銷活動
    /// </summary>
    [Route("api/call-campaign")]
    [ApiController]
    [SetAuthorizationFilter(Sh.AuthCode.不設限)]
    public class ReserveCampaignController : Controller
    {
        /// <summary>
        /// 取得行銷活動列表
        /// </summary>
        /// <param name="reserveCampaignName"></param>
        /// <param name="currentPage"></param>
        /// <param name="pageSize"></param>
        /// <param name="orderByName"></param>
        /// <param name="sort"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        [HttpGet("")]
        public async Task<object> ListAsync([FromQuery] string reserveCampaignName = "", [FromQuery] int? currentPage = 1, [FromQuery] int? pageSize = 20, [FromQuery] string orderByName = "OrderNo", [FromQuery] string sort = "asc")
        {
            if (pageSize > 500)
            {
                pageSize = 500;
            }

            if (!(sort == "asc" || sort == "desc"))
            {
                throw new CustomException(System.Net.HttpStatusCode.BadRequest, "sort只能是asc或desc");
            }

            var temp = new V_ReserveCampaign().GetType().GetProperty(orderByName);
            if (temp == null)
            {
                throw new CustomException(System.Net.HttpStatusCode.BadRequest, "不存在欄位");
            }

            Expression<Func<V_ReserveCampaign, bool>> q = p => p.Is_Deleted == "N"
                    && (string.IsNullOrEmpty(reserveCampaignName) || (p.ReserveCampaignName != null && p.ReserveCampaignName.Contains(reserveCampaignName)))
                    ;

            if (orderByName.ToLower().Trim() != "id")
            {
                orderByName += " " + sort + ", id desc";
            }
            else
            {
                orderByName += " " + sort;
            }

            var ct = NewContext.GvContext;
            var list = await ct.GetPageListAsync(q, columns: "Id, ReserveCampaignName, OrderNo, StartAt, EndAt, ModifierName, ModifyDate, CreatorName, CreateDate", page: currentPage ?? 1, pageSize: pageSize ?? 20, orderByName);
            //var list = await ct.GetPageListAsync(q, page: currentPage ?? 1, pageSize: pageSize ?? 20, orderByName + " " + sort);
            return list;
        }

        /// <summary>
        /// 取得行銷活動
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        [HttpGet("get")]
        public async Task<dynamic> GetAsync([FromQuery] int Id)
        {
            var res = await Ds.NewContext.GvContext.ReserveCampaigns.Where(r => r.Id == Id)
                .FirstOrDefaultAsync();

            if (res == null)
            {
                throw new CustomException(System.Net.HttpStatusCode.BadRequest, "查無資料 " + Id.ToString());
            }
            return res;
        }
                
        /// <summary>
        /// 建立行銷活動
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpPost("")]
        public async Task<object> CreateAsync(Dtos.CreateReserveCampaign dto)
        {
            var ct = NewContext.GvContext;
            var res = await Models.ReserveCampaignHelper.CreateReserveCampaignAsync(ct, dto);
            return res;
        }

        /// <summary>
        /// 編輯行銷活動
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpPatch("")]
        public async Task<object> UpdateAsync(Dtos.UpdateReserveCampaign dto)
        {
            var ct = NewContext.GvContext;
            var res = await Models.ReserveCampaignHelper.UpdateReserveCampaignAsync(ct, dto);
            return res;
        }

        /// <summary>
        /// 刪除行銷活動
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpDelete("")]
        public async Task<object> DeleteAsync([FromQuery] int id)
        {
            var res = await Ds.NewContext.GvContext.MarkDeleteAsync<Ds.Gv.ReserveCampaign>(id, Sh.ModifyInfo);
            return res;
        }
    }
}



再增加一個同步範例(只例出 action)


        /// <summary>
        /// 取得列表
        /// </summary>
        /// <param name="name"></param>
        /// <param name="currentPage"></param>
        /// <param name="pageSize"></param>
        /// <param name="orderByName"></param>
        /// <param name="sort"></param>
        /// <returns></returns>
        [HttpGet("")]
        public object List([FromQuery] string name = "", [FromQuery] int? currentPage = 1, [FromQuery] int? pageSize = 20, [FromQuery] string orderByName = "OrderNo", [FromQuery] string sort = "asc")
        {
            return "";
        }

        /// <summary>
        /// 取得明細資料
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        [HttpGet("get")]
        public object Get([FromQuery] int id)
        {
            return "";
        }

        /// <summary>
        /// 建立
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpPost("")]
        public object Create(Dtos.PhysicalCheckUpType dto)
        {
            return "";
        }

        /// <summary>
        /// 編輯
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpPatch("")]
        public object Update(Dtos.PhysicalCheckUpType dto)
        {
            return "";
        }

        /// <summary>
        /// 刪除
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <exception cref="CustomException"></exception>
        [HttpDelete("")]
        public object Delete([FromQuery] int id)
        {
            return 1;
        }
More...
Bike, 2023/12/13 上午 08:54:28
.Net 6 的 Captcha 實做
產生 FileStreamResult 物件的 function 如下: (目前置於 SU 之中,以便轉移)

        static List<Brush> CaptchaBrushes = null;
        public static FileStreamResult CreateCaptcha(string captcha)
        {
            if (CaptchaBrushes == null)
            {
                CaptchaBrushes = new List<Brush>();
                CaptchaBrushes.Add(Brushes.White);
                CaptchaBrushes.Add(Brushes.Gold);
                CaptchaBrushes.Add(Brushes.LightSkyBlue);
                CaptchaBrushes.Add(Brushes.LimeGreen);
                CaptchaBrushes.Add(Brushes.AliceBlue);
                CaptchaBrushes.Add(Brushes.AntiqueWhite);
                CaptchaBrushes.Add(Brushes.BurlyWood);
                CaptchaBrushes.Add(Brushes.Silver);
            }

            int width = 90;
            int height = 45;

            //https://stackoverflow.com/questions/61365732/cannot-access-a-closed-stream-when-returning-filestreamresult-from-c-sharp-netc
            //Using statements close and unload the variable from memory set in the using statement which is why you are getting an error trying to access a closed memory stream.You don't need to use a using statement if you are just going to return the result at the end.

            //這個 memory stream 不用關閉或 dispose
            var ms = new MemoryStream();

            // 釋放所有在 GDI+ 所佔用的記憶體空間 ( 非常重要!! )
            using (Bitmap _bmp = new Bitmap(width, height))
            using (Graphics _graphics = Graphics.FromImage(_bmp))
            using (Font _font = new Font("Courier New", 24, FontStyle.Bold)) // _font 設定要出現在圖片上的文字字型、大小與樣式
            {
                // (封裝 GDI+ 繪圖介面) 所有繪圖作業都需透過 Graphics 物件進行操作
                _graphics.Clear(Color.Black);

                // 如果想啟用「反鋸齒」功能,可以將以下這行取消註解
                //_graphics.TextRenderingHint = TextRenderingHint.AntiAlias;

                // 將亂碼字串「繪製」到之前產生的 Graphics 「繪圖板」上
                var x = 10;
                for(var i = 0; i < captcha.Length; i++)
                {
                    _graphics.DrawString(captcha.Substring(i, 1), _font, CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], x, Su.MathUtil.GetRandomInt(15));
                    x += 10 + Su.MathUtil.GetRandomInt(10);
                }

                // 畫線
                
                _graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
                    Su.MathUtil.GetRandomInt(0, Convert.ToInt32((width * 0.9 / 2))), 0, Su.MathUtil.GetRandomInt(Convert.ToInt32(width / 2), Convert.ToInt32(width * 1.9 / 2)), height);
                
                _graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
                    Su.MathUtil.GetRandomInt(Convert.ToInt32(width / 2), Convert.ToInt32(width * 1.9 / 2)), 0, Su.MathUtil.GetRandomInt(0, Convert.ToInt32((width * 0.9 / 2))), height);
                
                _graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
                    0,
                    Su.MathUtil.GetRandomInt(height / 2),
                    width,
                    height / 2 + Su.MathUtil.GetRandomInt(height / 2)
                    );

                _graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
                 0,
                 height / 2 + Su.MathUtil.GetRandomInt(height / 2),
                 width,
                    Su.MathUtil.GetRandomInt(height / 2)
                 );

                _bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            }

            ms.Seek(0, SeekOrigin.Begin);

            // Controller 的型別為 FileResult
            return new FileStreamResult(ms, "image/jpeg")
            { FileDownloadName = $"{DateTime.Now.Ymdhmsf()}.jpg" };
        }



輸出用的 Controller 和 Action 如下:

namespace Web.Controllers
{
    public class CaptchaController : Controller
    {
        [Route("captcha")]
        public async Task<FileStreamResult> Index()
        {
            //產生 Captcha 並存入 Session 之中。目前是四位數字
            string captcha = (await Ah.ReGetAsync<object>("api/kol/create-captcha-code")).ToString();

            //產生圖檔並回傳 FileStreamResult
            return Su.Wu.CreateCaptcha(captcha);
        }
    }
}
More...
Bike, 2022/9/25 下午 10:03:44
Postgres 中使用 Transaction (TransactionScope) 必需要自已實做重試的功能
在 Postgres 使用 TransactionScope 似乎很空易就會發生 Transaction Abort 的情況。所以必需自行實做 retry 的機制

            int tries = 0;
            while (tries < Su.PgSql.maxTransactionAborted)
            {
                try
                {
                    using (var scop = new System.Transactions.TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
                    {
                        var context = ShopBandContext.Context;

                        updateCount = await context.UpdateAsync<MarketDiscount>(dto, Sc.ModifyInfo,
                            onlyColumns: "StartAt,EndAt,NewPrice,FrontEndMemo,BackEndMemo");

                        scop.Complete();
                        break; //這個 Break 很重要
                    }
                }
                catch (Exception ex)
                {
                    // 我發現因為是非同步模式,所以要檢查很多層的 InnerException
                    if (tries < Su.PgSql.maxTransactionAborted
                        && ((ex.InnerException != null && ex.InnerException is Npgsql.PostgresException && ((Npgsql.PostgresException)ex.InnerException).SqlState == "40001")
                            || (ex.InnerException.InnerException != null && ex.InnerException.InnerException is Npgsql.PostgresException && ((Npgsql.PostgresException)ex.InnerException.InnerException).SqlState == "40001")))
                    {
                        //隨機休息 10 ~ 20 ms, 等另一個 job 完工
                        System.Threading.Thread.Sleep(Su.MathUtil.Random.Next(10, 20));
                        tries++;
                    }
                    else
                    {
                        throw;
                    }
                }
            }
More...
Bike, 2022/8/25 上午 09:19:35
關於 PostgreSQL
-- 自動更新某個欄位

CREATE OR REPLACE FUNCTION before_market_update ()
RETURNS TRIGGER AS 
$$
BEGIN
    NEW.LMDT = LocalTimeStamp;
    RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

CREATE TRIGGER updt_market
  BEFORE UPDATE
  ON "Market"
  FOR EACH ROW
  EXECUTE PROCEDURE before_market_update();
More...
Bike, 2022/8/12 下午 09:25:57
PostgreSQL 的 monitor trigger
--建立 table (要記得改 XXX)
CREATE TABLE IF NOT EXISTS public.table_monitor
(
    table_name text COLLATE pg_catalog."default" NOT NULL,
    update_at timestamp without time zone NOT NULL DEFAULT now(),
    update_count bigint NOT NULL DEFAULT 0,
    CONSTRAINT table_monitor_pkey PRIMARY KEY (table_name)
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.table_monitor
    OWNER to XXX;

-- 這一段只要執行一次
CREATE OR REPLACE FUNCTION public.table_monitor_trigger_fnc()
    RETURNS trigger
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE NOT LEAKPROOF
AS $BODY$
BEGIN
     Update public.table_monitor Set update_count = update_count + 1, update_at = now()  Where table_name = TG_TABLE_NAME; 
RETURN NEW;
END;
$BODY$;

ALTER FUNCTION public.table_monitor_trigger_fnc()
    OWNER TO XXX;

-- 對於被監控的 table, 要執行以下的指令,記得 monitor_table_name 要改為 table 的名字(要符合大小寫)
INSERT INTO public.table_monitor (table_name, update_at, update_count) VALUES ('monitor_table_name', '2000-01-01', 0) ON CONFLICT DO NOTHING;

CREATE OR REPLACE TRIGGER table_monitor_trigger
    AFTER INSERT OR DELETE OR UPDATE 
    ON monitor_table_name
    FOR EACH STATEMENT
    EXECUTE FUNCTION public.table_monitor_trigger_fnc();

監控用的程式碼:
public static void UpdateTableCache()
{
    //標記自已是正在執行的 Thread. 讓舊的 Thread 在執行完畢之後, 應該會自動結束. 以防舊的 Thread 執行超過 10 秒.
    CurrentThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

    try
    {
        //確認自已是正在執行的 Thread, 重覆執行. (另一個 Thread 插入執行)
        while (CurrentThreadId == System.Threading.Thread.CurrentThread.ManagedThreadId)
        {
            LastUpdateDate = DateTime.Now;

            foreach (var dbId in CachedDbs)
            {
                var sql = @"select * from table_monitor";

                var dt = Su.PgSql.DtFromSql(sql, dbId);

                foreach (DataRow row in dt.Rows)
                {
                    string changeId = row["update_count"].DBNullToDefault();
                    string CacheKey = TableCacheKey(dbId, row["table_name"].DBNullToDefault());
                    ObjectCache cache = MemoryCache.Default;

                    string OldValue = (string)cache[CacheKey];

                    if (OldValue == null)
                    {
                        cache.Set(CacheKey, changeId, DateTime.MaxValue);
                    }
                    else
                    {
                        if (changeId != OldValue)
                        {
                            cache.Remove(CacheKey);
                            cache.Set(CacheKey, changeId, DateTime.MaxValue);
                        }
                    }
                }
            }

            //每兩秒檢查一次
            System.Threading.Thread.Sleep(2000);
        }
    }
    catch (Exception)
    {
        //依經驗, 只要 DB 能通, 這裡幾乎不會有問題, 所以這裡暫時不處理, 未來有問題時可以考慮寫入文字檔比較好.
    }
}
More...
Bike, 2022/7/20 上午 10:05:39
.Net Framework 的檔案檢查機制 Web API 和 MVC 5
我們的目標是在啟動時,掛一個 global filter 來檢查上傳的檔案附檔名,避免上傳未預期的可執行檔。使用 global filter 可以防止程式設計師忘了做檔案檢查,發生危險。我們目前用到的 .Net Framework 有兩個類別,一個是 System.Web.Http.ApiController,另一個是 System.Web.Mvc.Controller,兩個要分開處理。

1. Web API (System.Web.Http.ApiController)
.Net Framework 的 Web API 2 在接收檔案時,必需使用 Request.Content.ReadAsMultipartAsync 把上傳的資料存入 MultipartMemoryStreamProvider 之後再來處理。
範例如下: 
    public class UploadController : ApiController
    {
        public async Task<object> PostFormData()
        {
            var provider = new MultipartMemoryStreamProvider();

            if (! Request.Content.IsMimeMultipartContent())
            {
                return "no file";
            }

            //要注意這裡的 await
            await Request.Content.ReadAsMultipartAsync(provider);

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
                    string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
                    if (System.IO.File.Exists(filename))
                    {
                        System.IO.File.Delete(filename);
                    }

                    using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
                    {
                        var contentStream = await content.ReadAsStreamAsync();
                        await contentStream.CopyToAsync(fileStream);
                        Trace.WriteLine("Save To" + filename);
                    }
                }
            }
            return "OK";
        }
    }


程式碼中有個地方我沒有注意到。我一開始是用 Request.Content.ReadAsMultipartAsync(provider); 把資料讀進 provider,但前方沒有加上 await,造成有時讀不到檔案的情況,再次提醒自己 Async 和 Await 的關係。

直覺的想法是在 filter 中執行一樣的程式碼,不存檔,只要抓出檔名來檢查即可。但這時遇到一個問題 Request.Content.ReadAsMultipartAsync 不能執行兩次,Google 了一陣子,也沒找不到類似 seek(0) 的操作。所以只好自己寫一個暫存的機製。

這裡決定使用 System.Web.HttpContext.Current.Items 來暫存 provider,最後的 OnActionExecutingAsync 結果如下:

        /// <summary>
        /// 檔案檢查
        /// </summary>
        /// <param name="actionContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            Trace.WriteLine("ApiCheckFile OnActionExecutingAsync");

            var request = actionContext.Request;
            if (!request.Content.IsMimeMultipartContent())
            {
                return;
            }

            var provider = new MultipartMemoryStreamProvider();
            await request.Content.ReadAsMultipartAsync(provider);

            //把 provider 存入 System.Web.HttpContext.Current.Items 之中,以便在 controller 中再度使用
            System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"] = provider;

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    var filename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    Trace.WriteLine(filename);

                    var ext = System.IO.Path.GetExtension(filename);
                    if (!".jpg,.jpeg,.png".Contains(ext.ToLower()))
                    {
                        throw new Exception("file format error.");
                    }
                }
            }

            return;
        }


而 controller 中的 Action 則改為以下的版本;本版本中,除了把上傳的檔案存檔以外,還有讀取 form data 中其它欄位的範例:

        public async Task<object> PostFormData()
        {
            //改由 HttpContext.Current.Items 中,讀取資料。
            MultipartMemoryStreamProvider provider = (MultipartMemoryStreamProvider)System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"];

            //如果沒有經過 filter,provider會是 null, 這時就要直接由 Request.Content 讀入 provider
            if (provider == null)
            {
                provider = new MultipartMemoryStreamProvider();
                Request.Content.ReadAsMultipartAsync(provider);
            }

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    Trace.WriteLine("FileName: " + localFilename);
                    Trace.WriteLine("FileName: " + @"~/App_Data/Temp/" + localFilename);
                    System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
                    string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
                    if (System.IO.File.Exists(filename))
                    {
                        System.IO.File.Delete(filename);
                    }

                    using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
                    {
                        var contentStream = await content.ReadAsStreamAsync();
                        await contentStream.CopyToAsync(fileStream);
                        Trace.WriteLine("Save To" + filename);
                    }
                }
                else
                {
                    var contentStream = await content.ReadAsStreamAsync();
                    var reader = new System.IO.StreamReader(contentStream);
                    var data = reader.ReadToEnd();
                    Trace.WriteLine("data: " + data);
                }
            }
            return "OK";
        }


以上是展示 Web Api 的 Filter。

如果是 MVC 的 Controller,就簡單多了,直接讀取 actionContext.HttpContext.Request.Files 裡面各檔案的 FileName 即可。不過也遇到了一個和直覺不同的地方,對 actionContext.HttpContext.Request.Files 居然不能用 foreach 請見下方註解掉的地方。也許是我的用法有問題,如果有人找到可以使用的方法也麻煩告知。

        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            Debug.WriteLine("MvcCheckFileFilter OnActionExecuting");
            if (actionContext.HttpContext.Request.Files.Count > 0)
            {
                for (int i = 0; i < actionContext.HttpContext.Request.Files.Count; i++)
                {
                    System.Web.HttpPostedFileBase file = actionContext.HttpContext.Request.Files[i];
                    if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
                    {
                        throw new Exception("file format error.");
                    }
                    Debug.WriteLine(i + "MvcCheckFileFilter OnActionExecuting File type: " + file.FileName.ToString());
                }
            }

            //以下寫法會發生錯誤: 無法將類型 'System.String' 的物件轉換為類型 'System.Web.HttpPostedFileBase'。
            //foreach (HttpPostedFileBase file in actionContext.HttpContext.Request.Files)
            //{
            //    if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
            //    {
            //        throw new Exception("file format error.");
            //    }
            //}
        }


最後是把 filter 掛在 global filter 裡,如下:
Web API Controller 的部份:
在 WebApiConfig.cs 裡加上 config.Filters.Add(new ApiCheckFile()); 見下圖:
 

附帶一提,Register 是在 global.asax 裡被叫用的。微軟的架構常常這樣,硬是拉到另一個目錄的獨立檔案裡,如果 global.asax 沒有這一行,不知所以的人,在 App_Start 裡,建立了一個 WebApiConfig.cs 然後會發現完全沒做用... 見下圖

 
MVC Controller 的部份,
在 FilterConfig.cs 中,加上 filters.Add(new MvcCheckFileFilter()); 如下圖
 

一樣是在 global.asax 中呼叫  FilterConfig.RegisterGlobalFilters,見下圖:

 
不過有趣的地方是,在 global.asax 裡, WebApiConfig.Register 和 FilterConfig.RegisterGlobalFilters 的叫用方法不同,一個是把 function 當作變數,一個是直接呼叫 function, 大家可以比較一下。

附註 1, 關於 OnActionExecutingAsync 和 OnActionExecuting:

Web API 的 filter 中,提供了 OnActionExecutingAsync 和 OnActionExecuting 兩個 method 可以 override ,我實驗的結果是如果兩個 method 都 override 了,只有 OnActionExecutingAsync 會被呼叫。

MVC 的 filter 中,只有 OnActionExecuting 可以 override。
 
More...
Bike, 2022/5/15 上午 10:01:11
使用 Gmail API 及 Google API 憑證的取得流程
這裡是我測試 Gmail API 和 Google API 憑証的一些記錄。

如果你的目的是要使用 Gmail Api 取代舊的 Gmail SMTP 來發送通知信,建議你先跳到最下方看一下結論。

如果你是想要看一下 Gmail API 和 Google API 憑証的使用方法,可以看一下這篇文章。

1. 在 google cloud platform 建立新的專案.
https://console.cloud.google.com/

 
 



啟用 Gmail API







 



 


 
 



因為我們要透過 OAuth  取得使用者授權,所以要設定使用 OAuth 的同意畫面。

 
指定授權的範圍
 

 
因為剛建立的專案,不會被公開,所以要指定測試使用者
 
如果要給任意使用者,必需經過發布的流程,但準備工作有點麻煩,所以這次就不發布了。
 




建立 OAuth 2.0 用戶端 ID 憑証
 
這裡除了名稱外,還有一個設定重導 Uri 的項目。現在不填寫,但稍後要回來補這個資料。
 
 
 下載 json 之後,命名為 client_secret.json 保留後續使用。


再來就要建立專案了. 用 VS2022 建立一個新專案
  


 




記錄網址, 本測試專案是 https://localhost:44340/ ,請依實際網址為準。



回到 OAuth 2.0 用戶端 ID 的設定頁. 在已授權的重新導向 URI 中填入 https://localhost:44340/Home/AuthReturn (填入的網址依實際專案的狀況,可能會有變化)
 
 

在 VS2022 中,使用 Nuget 安裝套件: (有漏的再麻煩和我說)
Google.Apis.Gmail.v1
Google.Apis.Auth
MimeKit (發送 gmail 時使用)

建立認証用的網址:
建立一個 Action, 用來取得認証用的網址:
        /// <summary>
        /// 取得授權的項目
        /// </summary>
        static string[] Scopes = { GmailService.Scope.GmailSend };

        // 和登入 google 的帳號無關
        // 任意值,若未來有使用者認証,可使用使用者編號或登入帳號。
        string Username = "ABC";

        /// <summary>
        /// 存放 client_secret 和 credential 的地方
        /// </summary>
        string SecretPath = @"D:\project\GmailTest\Data\Secrets";

        /// <summary>
        /// 認証完成後回傳的網址, 必需和 OAuth 2.0 Client Id 中填寫的 "已授權的重新導向 URI" 相同。
        /// </summary>
        string RedirectUri = $"https://localhost:44340/Home/AuthReturn";

        /// <summary>
        /// 取得認証用的網址
        /// </summary>
        /// <returns></returns>
        public async Task<string> GetAuthUrl()
        {
            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                FileDataStore dataStore = null;
                var credentialRoot = Path.Combine(SecretPath, "Credentials");
                if (!Directory.Exists(credentialRoot))
                {
                    Directory.CreateDirectory(credentialRoot);
                }

                //存放 credential 的地方,每個 username 會建立一個目錄。
                string filePath = Path.Combine(credentialRoot, Username);
                dataStore = new FileDataStore(filePath);

                IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
                    Scopes = Scopes,
                    DataStore = dataStore
                });

                var authResult = await new AuthorizationCodeWebApp(flow, RedirectUri, Username)
                .AuthorizeAsync(Username, CancellationToken.None);

                return authResult.RedirectUri;
            }
        }


執行結果:
 
用 chrome 開啟產生的網址: 


選取任一帳號,如果出現以下錯誤,請回到 "OAuth 同意畫面" 去新增測試使用者


 
因為應用程式尚未發布,所以會看到警告,勇敢的繼續下去


這裡會要求授權使用你的名義發送信件。(這是在程式中取得授權的項目 Scopes 中所指定的)


再繼續之後,會被重導至我們在 redirectUri  指定的網址。因為我們尚未完成,所以會看到錯誤,順便也可以看一下,會帶回哪一些參數。有 state, code, scope,共三個。


順便看一下,google 的套件會在 Credentials 的目錄下幫使用者建立一個目錄,在完成驗証前,會先放一個 System.String-oauth_XXX 的檔案,裡面的值和回傳的 state 是一樣的,這個應該是用來驗証回傳資料的。



接下來我們要新增 Action "AuthReturn" 如下:

        public async Task<string> AuthReturn(AuthorizationCodeResponseUrl authorizationCode)
        {
            string[] scopes = new[] { GmailService.Scope.GmailSend };

            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                //確認 credential 的目錄已建立.
                var credentialRoot = Path.Combine(SecretPath, "Credentials");
                if (!Directory.Exists(credentialRoot))
                {
                    Directory.CreateDirectory(credentialRoot);
                }

                //暫存憑証用目錄
                string tempPath = Path.Combine(credentialRoot, authorizationCode.State);            

                IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(
                new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
                    Scopes = scopes,
                    DataStore = new FileDataStore(tempPath)
                });

                //這個動作應該是要把 code 換成 token
                await flow.ExchangeCodeForTokenAsync(Username, authorizationCode.Code, RedirectUri, CancellationToken.None).ConfigureAwait(false);

                if (!string.IsNullOrWhiteSpace(authorizationCode.State))
                {                
                    string newPath = Path.Combine(credentialRoot, Username);
                    if (tempPath.ToLower() != newPath.ToLower())
                    {
                        if (Directory.Exists(newPath))
                            Directory.Delete(newPath, true);

                        Directory.Move(tempPath, newPath);
                    }
                }

                return "OK";
            }
        }


再跑一次上面的流程,最後回到 AuthReturn

在 D:\project\GmailTest\Data\Secrets\Credentials\ABC 裡面會產生一個檔案: 這個就是我們的 token 了。


看一下裡面的內容, 有 access_token, refresh_token, scope 等等, 用途應該很好猜了.. 不知道各項目的目途也沒有關係。只要有這個 token 就可以了。


refresh_token 的效期請參考以下文件:
https://developers.google.com/identity/protocols/oauth2 。也可以參考下圖, 若是要用 gmail api 來發送通知信(例如連絡我們),紅色的地方是比較令人困擾的,例如 6 個月以上,沒有人留言,原來留下的 refresh_token 就失效了。使用者必需重新建立一個 refresh_token 。

 
最後來使用 gmail api 發送通知信, 直接看程式碼如下: 在這個過程中遇到最大的問題除了憑証問題之外,另一個問題是編碼。直到最後找到可以用 MimeKit 把 System.Net.Mail.MailMessage 編碼成 Gmail API 的格式才解決。程式碼如下:

        public async Task<bool> SendTestMail()
        {
            var service = await GetGmailService();

            GmailMessage message = new GmailMessage();
            message.Subject = "標題";
            message.Body = $"<h1>內容</h1>";
            message.FromAddress = "bikehsu@gmail.com";
            message.IsHtml = true;
            message.ToRecipients = "bikehsu@gmail.com";
            message.Attachments = new List<Attachment>();

            string filePath = @"C:\Users\bike\Pictures\Vegetable_pumpkin.jpg";    //要附加的檔案
            Attachment attachment1 = new Attachment(filePath);
            message.Attachments.Add(attachment1);

            SendEmail(message, service);
            Console.WriteLine("OK");

            return true;
        }

        async Task<GmailService> GetGmailService()
        {
            UserCredential credential = null;

            var credentialRoot = Path.Combine(SecretPath, "Credentials");
            if (!Directory.Exists(credentialRoot))
            {
                Directory.CreateDirectory(credentialRoot);
            }

            string filePath = Path.Combine(credentialRoot, Username);

            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                GoogleClientSecrets.Load(stream).Secrets,
                Scopes,
                Username,
                CancellationToken.None,
                new FileDataStore(filePath));
            }

            var service = new GmailService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Send Mail",
            });

            return service;
        }


        public class GmailMessage
        {
            public string FromAddress { get; set; }
            public string ToRecipients { get; set; }

            public string Subject { get; set; }
            public string Body { get; set; }
            public bool IsHtml { get; set; }

            public List<System.Net.Mail.Attachment> Attachments { get; set; }
        }


        public static void SendEmail(GmailMessage email, GmailService service)
        {
            var mailMessage = new System.Net.Mail.MailMessage();
            mailMessage.From = new System.Net.Mail.MailAddress(email.FromAddress);
            mailMessage.To.Add(email.ToRecipients);
            mailMessage.ReplyToList.Add(email.FromAddress);
            mailMessage.Subject = email.Subject;
            mailMessage.Body = email.Body;
            mailMessage.IsBodyHtml = email.IsHtml;

            if (email.Attachments != null)
            {
                foreach (System.Net.Mail.Attachment attachment in email.Attachments)
                {
                    mailMessage.Attachments.Add(attachment);
                }
            }

            var mimeMessage = MimeKit.MimeMessage.CreateFromMailMessage(mailMessage);

            var gmailMessage = new Google.Apis.Gmail.v1.Data.Message
            {
                Raw = Encode(mimeMessage)
            };

            Google.Apis.Gmail.v1.UsersResource.MessagesResource.SendRequest request = service.Users.Messages.Send(gmailMessage, "me");

            request.Execute();
        }

        public static string Encode(MimeMessage mimeMessage)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                mimeMessage.WriteTo(ms);
                return Convert.ToBase64String(ms.GetBuffer())
                    .TrimEnd('=')
                    .Replace('+', '-')
                    .Replace('/', '_');
            }
        }



收到的信件:



結論:
使用 Gmail API 最大的原因是要增加安全性,和舊的 smtp 不同的地方是,使用 gmail api 之後,客戶不需要提供 gmail 的帳號和密碼就可以讓系統使用 gmail 發送信件,不過由於 refresh_token 的效期問題,可能會造成無法發送通知信而沒有任何人發現的情況,整個實用性會變的很低。

另一個還沒有測試的部份是應用程的啟用。這個審核不知道會不很麻煩,不過可以而知的時,整個流程會花更多的時間。

取代的做法: 可能要改用 Amazon 的 SES 來寄信,而且為了避免每個小網站都要跑 SES 的建立流程,準備來寫一個 API 給各網站使用,可以發送簡單的通知信。

以上的程式碼可以在這裡下載: https://github.com/bikehsu/GmailTest
More...
Bike, 2022/4/10 下午 09:31:15
IIS 配合 AD (Active Directory) 認証, 使用 .Net 6.0
環境說明:

AD Server: dc1 (192.168.101.109)
PC: pc110 (192.168.101.110)
PC: pc111 (192.168.101.111)

第一步,把 PC 加入 AD, 這個算是基本操作,網路上說明很多, 就不再截圖了。不過在這裡還是遇到了第一個問題,解決過程請參考另一份文件: https://blog.uwinfo.com.tw/Article.aspx?Id=486

第二步,在 Visual Studio 的測試環境中測試:
一開始是使用 .Net 6.0 來實作,沒想到找到的文件都是 .Net Core 3.1 的,所以先用 .Net Core 3.1 實做了一次,後來改用 .Net 6.0 實作才成功。使用 .Net 6.0 實作的過程如下:

1. 建立一個 MVC 的標準專案: 
 
為了避免憑証問題,所以拿掉了 HTTPS 的設定


2. 改寫 launchSettings.json:
iisSettings 中的 windowsAuthentication 改為 True, anonymousAuthentication 改為 false。如下圖:
 
3. 修改 Program.cs, 加入以下四行指令:
builder.Services.AddAuthentication(IISDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
(注意: UseAuthentication 要加在 UseAuthentication 之後, VS 2022 應該會提示要新增 using Microsoft.AspNetCore.Server.IISIntegration;)
 
4. 在 HomeController 增加一個 Action, 以讀取驗証資料:

        [Route("GetAuthenticatedUser")]
        [HttpGet("[action]")]
        public IdentityUser GetUser()
        {
            return new IdentityUser()
            {
                Username = User.Identity?.Name,
                IsAuthenticated = User.Identity != null ? User.Identity.IsAuthenticated : false,
                AuthenticationType = User.Identity?.AuthenticationType
            };
        }

        public class IdentityUser
        {
            public string Username { get; set; }
            public bool IsAuthenticated { get; set; }
            public string AuthenticationType { get; set; }
        }
 
5. 啟動時記得要改用 IIS Express (感覺早上花了兩三個小時在為了這個問題打轉):


6. 執行結果:

第三步,在 IIS 中安裝網站:
1. 在安裝 IIS 時,記得要勾選 windows 驗證


2. 安裝 .Net 6.0 的 Hosting Bundle
https://dotnet.microsoft.com/en-us/download/dotnet/6.0
 
3. 新增網站:
主機名稱留空白 (AD 驗証在網域內好像不會使用指定的主機名稱,這個有待後續再做確認)


如果沒有刪除預設網站,會遇到警告,直接確認即可.


要把 Default Web Site 關閉,再啟動測試站
 
要啟動 windows 驗証: 
在 web.config 中增加 
      <security>
        <authentication>
          <anonymousAuthentication enabled="false" />
          <windowsAuthentication enabled="true" />
        </authentication>
      </security>



修改 applicationHost.config:

檔案位置: %windir%\system32\inetsrv\config\applicationHost.config
這兩地方的 Deny 改為 Allow
<section name="anonymousAuthentication" overrideModeDefault="Deny" />
<section name="windowsAuthentication" overrideModeDefault="Deny" />

 
參考文件: https://docs.microsoft.com/zh-tw/iis/get-started/planning-for-security/how-to-use-locking-in-iis-configuration

3. 可以取得登入資訊如下:

4. 從 Domain 中另一台主機來存取,不用登入,自動取得目前登入者的資訊。
 
5. 從非網域主機連線: 會要求認証
 

目前遇到問題: 在網域中的電腦只能用主機名稱登入,非網域的電腦,才能夠使用網址登入。

測試專案下載: https://github.com/bikehsu/AD60
 
More...
Bike, 2022/3/19 下午 09:10:08
在 CentOS 上面啟動多個 .Net Core 的網站
1. 參考這裡設定第一個網站:
https://blog.johnwu.cc/article/centos-asp-net-core-neginx.html
注意, 文章中有一個錯誤:
/etc/nginx/conf.d/my-website.conf 的第 27 行, 應該是 include /etc/nginx/conf.d/default_proxy_settings;

2.  因為要避開 5000 port, 所以修改第二個網站的 appsettings.json, 讓第二個網站開在 5002 port, 如下.
{
"Logging": {
    "LogLevel": {
     "Default": "Information",
     "Microsoft": "Warning",
     "Microsoft.Hosting.Lifetime": "Information"
    }
},
"Kestrel": {
        "EndPoints": {
                "Http": {
                        "Url": "http://localhost:5002"
                }
        }
},
"AllowedHosts": "*"
}


3. 新增 /etc/nginx/conf.d/my-website2.conf, 要注意
    A. portal2
    B. server localhost:5002
    C. server_name coretest2.bike.idv.tw
    當然 SSL 憑証的檔名也要記得改.

upstream portal2 {
    # localhost:5000 改成 ASP.NET Core 所監聽的 Port
    server localhost:5002;
}

server {
    # 只要是透過這些 Domain 連 HTTP 80 Port,都會轉送封包到 ASP.NET Core
    listen 80;
    # 可透過空白區分,綁定多個 Domain
    server_name coretest2.bike.idv.tw;
    location / {
        proxy_pass http://portal2/;
        include /etc/nginx/conf.d/default_proxy_settings;
    }
}

# 用 HTTPS 必須要有 SSL 憑證,如果沒有要綁定 SSL 可以把下面整段移除
server {
    # 只要是透過這些 Domain 連 HTTPS 443 Port,都會轉送封包到 ASP.NET Core
    listen 443 ssl;
    server_name coretest2.bike.idv.tw;
    ssl_certificate /etc/nginx/ssl/coretest2.bike.idv.tw.crt;
    ssl_certificate_key /etc/nginx/ssl/coretest2.bike.idv.tw.key;

    location / {
        proxy_pass http://portal2/;
        include /etc/nginx/conf.d/default_proxy_settings;
    }
}


改完後就可以用了.

以下是 https://blog.johnwu.cc/article/centos-asp-net-core-neginx.html 抄過來的一些檔案, 作為備份:

setup-aspnet-core.sh
#!/bin/bash

main() {
    sudo yum -y install epel-release
    sudo yum -y update

    install_nginx
    install_dotnet

    sudo firewall-cmd --add-service=http --permanent
    sudo firewall-cmd --add-service=https --permanent
    sudo firewall-cmd --reload
}

install_nginx() {
    echo "###################################"
    echo "########## Install Nginx ##########"
    echo "###################################"
    sudo yum -y install httpd-tools nginx
    sudo setsebool -P httpd_can_network_connect on
    sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
    sudo setenforce 0
    sudo systemctl enable nginx
    sudo systemctl restart nginx
}

install_dotnet() {
    echo "###########################################"
    echo "########## Install .NET Core 2.2 ##########"
    echo "###########################################"
    sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm
    sudo yum -y install aspnetcore-runtime-2.2
}

main "$@"


用以下指令執行:
sudo sh setup-aspnet-core.sh



/etc/systemd/system/my-website.service, (/bin/dotnet 有可能是 /usr/bin/dotnet)
[Unit]
# Description=<此服務的摘要說明>
Description=MyWebsite

[Service]
# WorkingDirectory=<ASP.NET Core 專案目錄>
WorkingDirectory=/usr/share/my-website

# ExecStart=/bin/dotnet <ASP.NET Core 起始 dll>
ExecStart=/bin/dotnet MyWebsite.dll

# 啟動若失敗,就重啟到成功為止
Restart=always
# 重啟的間隔秒數
RestartSec=10

# 設定環境變數,注入給 ASP.NET Core 用
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target


服務相關指令:
# 開啟,開機自動啟動服務
systemctl enable my-website.service

# 關閉,開機自動啟動服務
systemctl disable my-website.service

# 啟動服務
systemctl start my-website.service

# 重啟服務
systemctl restart my-website.service

# 停止服務
systemctl stop my-website.service

# 查看服務狀態
systemctl status my-website.service


/etc/nginx/conf.d/default_proxy_settings
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;


/etc/nginx/conf.d/my-website.conf
upstream portal {
    # localhost:5000 改成 ASP.NET Core 所監聽的 Port
    server localhost:5000;
}

server {
    # 只要是透過這些 Domain 連 HTTP 80 Port,都會轉送封包到 ASP.NET Core
    listen 80;
    # 可透過空白區分,綁定多個 Domain
    server_name demo.johnwu.cc example.johnwu.cc;
    location / {
        proxy_pass http://portal/;
        include /etc/nginx/conf.d/default_proxy_settings;
    }
}

# 用 HTTPS 必須要有 SSL 憑證,如果沒有要綁定 SSL 可以把下面整段移除
server {
    # 只要是透過這些 Domain 連 HTTPS 443 Port,都會轉送封包到 ASP.NET Core
    listen 443 ssl;
    server_name demo.johnwu.cc;
    ssl_certificate /etc/nginx/ssl/demo.johnwu.cc_bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/demo.johnwu.cc.key;

    location / {
        proxy_pass http://portal/;
        include /etc/nginx/conf.d/default_proxy_settings;
    }
}


Nginx 重新啟動:
# 檢查 Nginx 的設定是否有誤
nginx -t

# 若沒有錯誤,即可套用
nginx -s reload
More...
Bike, 2020/7/7 上午 08:01:43
|< 12345 >|
頁數 1 / 5 下一頁
~ Uwinfo ~