2022年12月20日 星期二

[React]用 react-i18next 實現多國語言

此篇主要說明如何讓網站實現多國語言,範例程式碼可參考這裡


主要核心元件為react-i18next,透過設定的方式來實現多國語言的切換,想要知道更多的內容可以參考官網


那麼就教學開始!

首先需要先安裝i18Nextreact-i18Nexti18Next-http-backendi18Next-borwser-languagedetector

指令碼↓↓↓↓↓↓↓

npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector --save


再來看一下最後完工的檔案結構,紅色是要新增的部分

src
├─locales
│  ├─en
│  │  └─translation.json
│  └─zh
│    └─translation.json
│ 
├─app.tsx 
├─index.tsx (要import等等設定好的i18n) 
├─package.json
└─i18n.ts

先新增語言檔,分成en和zh(中文都只用繁體中文,所以不用在加-Tw)


---------------locales/en/translation.json------------------
{
  "SwitchLan": "Switch Language",
  "Login": {
    "SignIn": "Sign in",
    "Account": "Account",
    "Password": "Password",
    "Remember": "Remember me",
    "ForgetPassword": "Forgot password?"
  }
}

---------------locales/zh/translation.json------------------
{
  "SwitchLan": "切換語言",
  "Login": {
    "SignIn": "登入",
    "Account": "帳號",
    "Password": "密碼",
    "Remember": "記住我",
    "ForgetPassword": "忘記密碼?"
  }
}


再來新增i18n.ts(i18n設定檔)

--------------------------i18n.ts------------------------------
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import translationEN from './locales/en/translation.json';
import translationZH from './locales/zh-tw/translation.json';

const resources = {
    en: {
        translation:translationEN,
    },
    zh: {
        translation:translationZH
    }
};

i18n
    // load translation using http -> see /public/locales
    // learn more: https://github.com/i18next/i18next-http-backend
    .use(Backend)
    // detect user language
    // learn more: https://github.com/i18next/i18next-browser-languageDetector
    .use(LanguageDetector)
    // pass the i18n instance to react-i18next.
    .use(initReactI18next)
    // init i18next
    // for all options read: https://www.i18next.com/overview/configuration-options
    .init({
        resources,
        fallbackLng: 'en',
        lng:"zh",
        debug: true,
        interpolation: {
            escapeValue: false, // not needed for react as it escapes by default
        },
    });

export default i18n;
----------------------------------------------------------------

打開index.tsx,把i18n設定檔import進去(紅字為修改部分)


--------------------------index.tsx------------------------------
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './i18n';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

...
-------------------------------------------------------------------

這樣設定就完成了,再來就拿Login頁面來實作,我們使用的是useTranslation(Hook)方式

簡單來說就是使用const{t}=useTranslation()後在用t('Key')去抓取對應的文字
p.s. 如果要多層結構以.隔開,例如t('login.key')

下方紅字為登入頁異動部分(有...代表被省略,請勿直接複製貼上)


--------------------------Login.tsx------------------------------

...
import UserContext from '../contexts/UserContext';
import { useTranslation } from 'react-i18next';
...

export default function Login() {
    //登入訊息
    const [LoginMsg, setLoginMsg] = useState<string>('');
    //登入資訊Context
    const userContext:any = useContext(UserContext)
    //登入確認
    const handleSubmit = ...
    //i18n
    const { t,i18n } = useTranslation();

 return (
        <ThemeProvider theme={theme}>
            <Grid container component="main" sx={{ height: '100vh' }}>
                <CssBaseline />
                <Grid
                    item
                    xs={false}
                    sm={4}
                    md={7}
                    sx={{
                        backgroundImage: 'url(https://source.unsplash.com/random)',
                        backgroundRepeat: 'no-repeat',
                        backgroundColor: (t) =>
                            t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
                        backgroundSize: 'cover',
                        backgroundPosition: 'center',
                    }}
                />
                <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
                    <Box
                        sx={{
                            my: 8,
                            mx: 4,
                            display: 'flex',
                            flexDirection: 'column',
                            alignItems: 'center',
                        }}
                    >
                        <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
                            <LockOutlinedIcon />
                        </Avatar>
                        <Typography component="h1" variant="h5">
                            {t('Login.SignIn')}
                        </Typography>
                        <Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
                            <TextField
                                error={LoginMsg !== ''}
                                margin="normal"
                                required
                                fullWidth
                                id="account"
                                label={t('Login.Account')}
                                name="account"
                                autoFocus
                            />
                            <TextField
                                error={LoginMsg !== ''}
                                margin="normal"
                                required
                                fullWidth
                                name="password"
                                label={t('Login.Password')}
                                type="password"
                                id="password"
                                autoComplete="current-password"
                                helperText={LoginMsg !== '' ? LoginMsg:""}
                            />
                            <FormControlLabel
                                control={<Checkbox  value="remember" color="primary" />}
                                label={t('Login.Remember')}
                                name="remember"
                            />
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                sx={{ mt: 3, mb: 2 }}
                            >
                                {t('Login.SignIn')}
                            </Button>
                            <Grid container>
                                <Grid item xs>
                                    <Link href="#" variant="body2">
                                        {t('Login.ForgetPassword')}
                                    </Link>
                                </Grid>
                                <Grid item>
                                    <Link href="#" variant="body2" onClick={() => { i18n.changeLanguage(i18n.language=="en"?"zh":"en") }}>
                                        {t('SwitchLan')}
                                    </Link>
                                </Grid>
                            </Grid>
                            <Copyright sx={{ mt: 5 }} />
                        </Box>
                    </Box>
                </Grid>
            </Grid>
        </ThemeProvider>
    );


--------------------------------------------------------------------


這樣登入頁就有多國語系了,這次在按鈕的右下有加入一個連結來實現切換功能。

因預設zh,所以會先顯示中文


點選"切換語言",就會變成英文頁面。


這樣就完成了多國語言的實作,只要在需要的頁面使用useTransaltion()就可以使用

而且i18next還有許多優點以及功能,例如自動偵測瀏覽器語言(需載入i18next-browser-languagedetector)、下次進入時會抓取上次的語言設定等等,未來如果有更多實作會在持續更新。






















2022年12月16日 星期五

[React & .Net Core]環境佈署

 


此篇主要說明如何佈署至測試以及正式環境,範例程式碼可參考這裡


此次佈署的教學先以Windows IIS以及Azure App Services來做教學,且需要搭配Visual Studio的publish功能,使用Scripts的方式就後續有使用VS Code開發時再做說明。

1.Window IIS

前言:

因有些WebForm、WebService等等舊專案仍會用到,所以現有客戶的測試或正式環境都會是Windows Server,但未來如果升級.Net Core就可不設限。

環境準備:

(1)安裝.Net 6 Runtime(傳送門)

(2)安裝Node.js(傳送門,版本選擇當下建議的下載即可)


佈署步驟:

先對專案點右鍵,再點"Publish..."


點選Folder方式(如果有權限可以直接連到IIS,也可選擇WebServer)

再來輸入發佈位置(也可以直接輸入網路磁碟路徑),先用預設就好,再點選"Finish"


就將發佈檔設定好了,實體檔案會在Properties/PublishProfiles內,現在只要點選"Publish"就可發佈至設定的資料夾內,檔案結構如圖二

圖一

圖二

接下來就將這些檔案放到IIS站台對應的資料夾內就OK囉。


2.Azure App Service

前言:

雲端的服務越來越完善且簡單設置,當然要有雲端的教學阿!


環境準備:

1.Azure 帳號開一個App Service(建立步驟可直接上網查,有很多教學資源)


佈署方式可以分成兩種,分別是手動佈署和自動佈署:

而手動佈署也可以分成兩種,一種是用VS直接發佈至Azure,另一種是用FTP去上傳實體檔案。

(1)VS直接部屬至Azure


一樣使用VS對專案右鍵點選後在點選"publish"

如果原本已經有佈署過則會先顯示上次佈署的設定,,再來點選"+New"。

Target請選擇"Azure"
Specific target請選擇Azure App Service(.NetFramework只能選擇Windows,.Net則不限制)


登入Azure帳戶,登入後就可以顯示出Azure上已有的AppService。


Deployment Type選擇Publish,點選Finish就完成Publish的設定。


完成可以看到一些發佈的設定,再點選Publish按鈕就可以發佈至Azure上。



(2)用FTP上傳實體佈署檔


前面步驟與IIS發佈類似,只是要先發佈至Local。


打開Azure App Service的Deployment Center,再選擇FTPS credentials。

頁面上可以看到FTP的一些相關資訊,使用FileZilla連線到Azure資料夾。


之後將剛剛發佈到Local檔案透過FileZilla上傳到wwwroot,就成功佈署了。

p.s.以上的手動佈署都有可能遇到佈署時會出現"檔案使用中"的錯誤訊息,這時可以先在資料夾內新增一個檔名是app_offline.htm的檔案,這樣就會強制關掉站台,待更新後再將此檔案Rename或Remove站台就會啟動了。


而自動佈署則是會用Azure的CI功能。(前提是要用它可以選到的版控,內部Gitlab不行)

此範例是用Github來教學。

打開Azure的Deployment Center,在Setting頁籤的Source點選Github。



登入Github,填入Github的相關設定


上面設定的Branch會針對設定的Branch去偵測Commit,只要有Commit就會自動佈署。
(正式環境建議拆成獨立分支,例如Release)

設定完之後,在Github的Action頁籤就可以看到一個yml檔案


試著Commit&push,就可以看到有一個workflow在run

點進去就可以看到目前CI的流程以及正在執行的步驟。

過一陣子就可以看到執行完成囉。


點選deploy網址就可以看到成功佈署於Azure上了(未來再也不用手動更版了!!!!!)
也可以查看Azure 的Deplotmeny Center的Logs看到發佈紀錄




總結一下, 佈署方式還是要依照客戶需求去選擇,但CI/CD是未來不可不學的一部分,如果環境許可的話都一律建議使用自動佈署搭配雲端的方式。













2022年12月15日 星期四

[React & .Net Core]React加入登入機制+串接API

 

這篇主要是說明如何從React登入機制和串接,範例程式碼可參考這裡


上一篇已實作了API權限的驗證和派發機制,現在要說明如何從React去呼叫登入的API


開始前先說明一下要實現的React登入架構:

(1)登入後取得的登入資訊(含Token)會放入client端的LocalStorage(如果不跨網頁可存在SessionStorage)

(2)使用React.UseContext方式讓全系統都可使用登入資訊。

(3)需要驗證的API都須在Header加上Authorization: Bearer {TOKEN}


首先先修改setupProxy.js,改成只要/api開頭的請求都會用代理伺服器轉到真正API的位置。

setupProxy.js
------------------------------------------------------
const context =  [
    "/api",
];
------------------------------------------------------

再來clientapp內需要新增或修改幾個檔案,列在下方(因程式碼太多,附上Github的檔案連結)

/services/APIService.ts 

=呼叫API的方法定義成共用的Service。

/services/AuthService.ts

=登入相關的一些方法,例如登入、登出和驗證...等等。

/contexts/UserContext.tsx

=登入機制用的Context,相關登入的驗證都會在這裡。

/models/_common/basicHttp.d.ts

=共用的傳輸模板結構。

/models/_common/Props.d.ts

=React Hook的properties結構

/models/_common/user.d.ts

=使用者登入資訊結構

/models/login/SignIn.d.ts

=登入API的資料結構

/pages/Login.tsx

=登入頁(使用mui的免費template去修改的)

/pages/About.tsx

=改成會去呼叫WeatherForecast.Get去取得資料後,顯示在畫面上。


API部分也要新增或修改一些結構

/Models/Common/BasicHttpViewModel.cs

=共用的傳輸模板結構。

/Models/SignInViewModel.cs

=登入API的資料結構

/Controllers/LoginController.cs

=針對signin的方法做一些調整。

/Controllers/WeatherForecastController.cs

=針對原本範例的Get作一些包裝,例如用BasicResponse包住。


照上面的步驟做完後,就可以F5試試看,畫面會如同下方圖片。

登入頁(帳密非空白就可登入)
登入後就可以用F12去查看Application/localstorage內是否儲存key="User"

p.s.目前範例專案沒有登出功能,如果要強制登出可以手動刪除localstorage的資料在重新整理即可跳回登入頁。

以上就完成了登入功能了,可以前往About頁面抓資料(需要驗證),看起來是有成功抓到資料!

通過API的Token驗證!


這樣就完成一個有登入驗證機制的網頁了!

但其實還是有很多尚未實作的部分,例如連接資料庫、登出或菜單頁的功能...等等。

也是等未來有實作後再分享給大家。





















2022年12月14日 星期三

[React & .Net Core]API驗證機制實作說明

 

這篇主要是說明如何將API加入驗證機制,範例程式碼可參考這裡


首先說明一下API的驗證機制是如何實現的,

目前驗證機制比較常用的有兩種,分別是Session-Based AuthenticationToken-Based Authentication

(1)Session-Based Authentication


Session-Based Authentication,顧名思義就是利用 Session 來進行驗證的機制,整體流程如上圖所示。客戶端先將帳號密碼丟給伺服器端,伺服器端透過帳密驗證,確定是正確的使用者登入,就開始建立一個伺服器端與你之間連線的 Session(會期)資料,裡面會放置一些用來辨識這個 Session 是被哪個使用者建立的資料,接著會回傳該 Session 的 ID 回來,並要求客戶端未來要驗證時需要帶這個 Session 的 ID 在 HTTP Cookie 上。
這個 HTTP Cookie 是位於 HTTP request 的 header 中的一個小區塊,它會自動被存在客戶端(通常是瀏覽器)上,並且 Cookie 會記載該資料是來自於哪個網址的資料,在瀏覽器對該網址為基底的網址做 HTTP request 的時候,都會自動帶上這個 Cookie ,藉以讓伺服器能夠驗證身份,並透過伺服器端儲存的 Session 資料,去回傳相對應身份可以得到的資料,這一整個就是基本的 Session-Based Authentication 架構。

(2)Token-Based Authentication


Token-Based Authentication,顧名思義就是利用 Token 來進行驗證的機制,整體流程如上圖所示。基本上流程和 Session-Based Authentication 類似,但是伺服器端不需要另外儲存任何資料,而是把相關的資料透過密鑰簽署組合成 Token,交由客戶端保管。客戶端拿到 Token 後,只要在每次 HTTP request 發送的時候,在 header 中的 Authorization 區塊帶上 Token 給伺服器端,即可被伺服器端再次透過密鑰驗證是否 Token 沒有被修改過,進而從中抽取資料,了解是哪個使用者的請求,給予相對應身份可以得到的資料。

在 Token-Based Authentication 中,目前最常見的方式是用 JWT(JSON Web Token)去做為 Token 的格式,JWT 會將 Token 分成三段,分別是 Header、Payload 和 Signature,而其中 Payload 就是上述所說到會放置能夠辨識使用者資料所儲存的地方。另外要注意的是,JWT 所使用的 Token 只會使用密鑰進行簽署的動作,簽署完後形成的就是 Signature,伺服器在收到 Token 時僅使用密鑰去進行 Signature 驗證,確定該 Token 是否有被修改過,並不會對 Token 進行加密。如果有要對 Token 進行加密的話,可以選擇 JWE(JSON Web Encryption)的加密方式。

優缺點比較

兩種不同的 Authentication 皆有一些不同的優缺點,底下就列舉幾點讓大家了解一下:

  1. Session-Based Authentication 由於是利用 Cookie 傳遞資料,Cookie 只要是在同網域下的請求皆會帶上,故只要別人用它的網站去連結你 API 的網址(例如:刪除題目的 API 網址),就可以讓你不知不覺對你的網站進行資料庫操作的動作,這個攻擊稱作 CSRF(Cross Site Request Forgery)。最簡單的防止方式通常是將 SameSite 加註在 Cookie 上,讓瀏覽器僅會在發送的請求是與該網頁同網域的 URI 的時候,才會帶上 Cookie 在請求中。關於詳細的攻擊內容以及防止方式可見此文章
  2. Token-Based Authentication 的 Token 通常必須要由客戶端自行決定如何儲存。由於最常見的客戶端通常為瀏覽器,所以通常是使用 JavaScript 去進行儲存。那如果在你的網頁上可以被別人加上 JavaScript 程式碼的話,就可以盜取別人的 Token,又由於伺服器端完全信任 Token 的關係,幾乎完全無法防止別人使用該 Token 去當作你的身份,用此身份來進行伺服器操作,這種攻擊通常稱為 XSS(Cross-site scripting)。最常見的防止方式是將 Token 一樣使用 Cookie 去帶,並且對該 Cookie 使用 HttpOnly 和 Secure Flag 讓 Cookie 內容不能被 JavaScript 讀取。詳細關於 XSS 的內容可見 Wiki
  3. 如果你所處的網路環境正在被監聽的話,不管是 Session-Based Authentication 或是 Token-Based Authentication 都有可能因為被監聽而 Cookie 或是 Token 被其他人偷走,進而導致帳號被盜,這種攻擊一般稱作 MitM(Man-in-the-Middle Attack)。通常的解決方式就是讓客戶端與伺服器端連線之間透過加密資料進行溝通,最常見的加密方式是使用 SSL(Secure Sockets Layer)加密技術,而溝通的 protocol 就會從 http 改成後面加上 SSL 第一個字的 https。詳細關於 MitM 的部分可以參見 Wiki
  4. 以 RESTful API 的角度看,我們曾經提到它有個原則叫做 Stateless,就是只要我送給伺服器的參數相同,應該就會得到一樣的結果,伺服器並沒有記錄任何狀態下來,去影響客戶端送出的 Request 要回應的內容。以這個特點來看,Token-Based Authentication 就比較符合這個原則,而 Session-Based Authentication 就會比較違反這個原則。
  5. Session-Based Authentication 由於在每次驗證建立後,都必須使用空間來儲存辨識使用者的資料;而 Token-Based Authentication 則是將這些資料交給客戶端儲存。如果以伺服器可以服務的用戶數量的量級來比較的話,就是 Token-Based Authentication 的方式能夠服務的用戶數量會高很多。但相對地,就會有第三點所提到的問題,伺服器端無法主動終止該 Token 的驗證,必須等到 Token 所含的過期時間到了才能被終止,所以通常的做法就是讓 Token 的過期時間很短,以防止 Token 被偷,那這樣對使用者的影響就是常常會需要重新輸入帳號密碼去做登入,就比較不適合用於需要長時間驗證的服務上。

綜合以上的優缺點比較,可得出以下結論

1.單伺服器的情況下,Session-Base和Token-Base可以擇一使用。
2.多伺服器的情況下,建議使用Token-Base驗證。

以此專案的某些考量,個人最後決定使用Token-Base Authentication當作API的驗證機制。
首先先來處理後端部分,有產生Token和檢查Token兩部分須實作
先處理檢查Token的機制

打開appsettings.json,加入JWT的相關設定

//JWT設定
---------------------------------------------
"JwtSettings": {
    "Issuer": "FirstReact",
    "SignKey": "1234567890123456789012",
    "ExpireMinutes": 30
  }
---------------------------------------------

然後打開program.cs,設定目前權限的驗證方式

Program.cs(位置放在builder.Services.AddControllersWithViews()的後面)
-------------------------------------------------------------------------------
#region Token-Base Authentication
builder
    .Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                // 當驗證失敗時,回應標頭會包含 WWW-Authenticate 標頭,這裡會顯示失敗的詳細錯誤原因
                options.IncludeErrorDetails = true; // 預設值為 true,有時會特別關閉
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    // 透過這項宣告,就可以從 "sub" 取值並設定給 User.Identity.Name
                    NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
                    // 透過這項宣告,就可以從 "roles" 取值,並可讓 [Authorize] 判斷角色
                    RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
                    // 一般我們都會驗證 Issuer
                    ValidateIssuer = true,
                    ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),
                    // 通常不太需要驗證 Audience
                    ValidateAudience = false,
                    //ValidAudience = "JwtAuthDemo", // 不驗證就不需要填寫
                    // 一般我們都會驗證 Token 的有效期間
                    ValidateLifetime = true,
                    // 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
                    ValidateIssuerSigningKey = false,
                    // "1234567890123456" 應該從 IConfiguration 取得
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
                };
            });
builder.Services.AddAuthorization();
#endregion
-------------------------------------------------------------------------------

下方再加上驗證和授權(須放在UseRouting後面)

-------------------------------------------------------------------------------
app.UseAuthentication();
app.UseAuthorization();
-------------------------------------------------------------------------------

將需要驗證的Controller或Action加上[Authorize],如下面範例

這樣檢查Token的部分就完成了,可以使用偵錯模式試試看,沒驗證成功會固定返回[401]Unauthorized


再來處理產生Token那段,在Services資料夾內新增JwtService.cs

JwtService.cs
-------------------------------------------------------------------------------

using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
namespace FirstReact.Services
{
    public class JwtService
    {
        private readonly IConfiguration Configuration;
        public JwtService(IConfiguration configuration)
        {
            this.Configuration = configuration;
        }
        public string GenerateToken(string MemberID, int expireMinutes = 30)
        {
            int configExpireMinutes= Configuration.GetValue<int>("JwtSettings:ExpireMinutes");
            if (configExpireMinutes==0) {
                configExpireMinutes = expireMinutes;
            }
            var issuer = Configuration.GetValue<string>("JwtSettings:Issuer");
            var signKey = Configuration.GetValue<string>("JwtSettings:SignKey");
            // 設定要加入到 JWT Token 中的聲明資訊(Claims)
            var claims = new List<Claim>();
            // 在 RFC 7519 規格中(Section#4),總共定義了 7 個預設的 Claims,我們應該只用的到兩種!
            //claims.Add(new Claim(JwtRegisteredClaimNames.Iss, issuer));
            claims.Add(new Claim(JwtRegisteredClaimNames.Sub, MemberID)); // User.Identity.Name
            //claims.Add(new Claim(JwtRegisteredClaimNames.Aud, "The Audience"));
            //claims.Add(new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds().ToString()));
            //claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())); // 必須為數字
            //claims.Add(new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())); // 必須為數字
            claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); // JWT ID
            // 網路上常看到的這個 NameId 設定是多餘的
            //claims.Add(new Claim(JwtRegisteredClaimNames.NameId, userName));
            // 這個 Claim 也以直接被 JwtRegisteredClaimNames.Sub 取代,所以也是多餘的
            //claims.Add(new Claim(ClaimTypes.Name, userName));
            // 你可以自行擴充 "roles" 加入登入者該有的角色
            claims.Add(new Claim("roles", "Admin"));
            claims.Add(new Claim("roles", "Users"));
            var userClaimsIdentity = new ClaimsIdentity(claims);
            // 建立一組對稱式加密的金鑰,主要用於 JWT 簽章之用
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));
            // HmacSha256 有要求必須要大於 128 bits,所以 key 不能太短,至少要 16 字元以上
            // https://stackoverflow.com/questions/47279947/idx10603-the-algorithm-hs256-requires-the-securitykey-keysize-to-be-greater
            var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
            
            // 建立 SecurityTokenDescriptor
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Issuer = issuer,
                //Audience = issuer, // 由於你的 API 受眾通常沒有區分特別對象,因此通常不太需要設定,也不太需要驗證
                //NotBefore = DateTime.Now, // 預設值就是 DateTime.Now
                //IssuedAt = DateTime.Now, // 預設值就是 DateTime.Now
                Subject = userClaimsIdentity,
                //Token有效時間,如有設定已設定優先
                Expires = DateTime.Now.AddMinutes(configExpireMinutes),
                SigningCredentials = signingCredentials
            };
            // 產出所需要的 JWT securityToken 物件,並取得序列化後的 Token 結果(字串格式)
            var tokenHandler = new JwtSecurityTokenHandler();
            var securityToken = tokenHandler.CreateToken(tokenDescriptor);
            var serializeToken = tokenHandler.WriteToken(securityToken);
            return serializeToken;
        }
    }
}

-------------------------------------------------------------------------------

在Program.cs裡加入下面的程式碼(須放在var app=builder.Build()前面)

//JWT簽發
builder.Services.AddSingleton<JwtService>();

在Models內新增SignInViewModel,等等方法會使用到

SignInViewModel.cs
-------------------------------------------------------------------------------
namespace FirstReact.Models
{
    public record SignInRequestViewModel(string Account,string Password);
    public record SignInResponseViewModel(string Token);
}
-------------------------------------------------------------------------------

在Controllers內新增LoginController.cs,裡面會內含一個SignIn的Action

LoginController.cs
-------------------------------------------------------------------------------
using FirstReact.Models;
using FirstReact.Services;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace FirstReact.Controllers
{
    [ApiController]
    [Route("api/[controller]/[Action]")]
    public class LoginController : ControllerBase
    {
        private readonly JwtService _jwt;
        private readonly ILogger<WeatherForecastController> _logger;
        public LoginController(ILogger<WeatherForecastController> logger, JwtService jwt)
        {
            _logger = logger;
            _jwt=jwt;
        }
        [HttpPost]
        public string SignIn(SignInRequestViewModel source)
        {
            string ret = "";
            if (
                source!=null 
                && !string.IsNullOrEmpty(source.Account)
                && !string.IsNullOrEmpty(source.Password)
                ) {
                SignInResponseViewModel res = new(_jwt.GenerateToken(source.Account));
                ret = JsonSerializer.Serialize(res);
            }
            return ret;
        }
    }
}

-------------------------------------------------------------------------------

這樣就實作好會發Token的登入API,使用Postman去呼叫看看,確實有回傳Token。

用Token去呼叫剛剛需要驗證的API,加入方式是在HTTP Request Header 加入 Authorization: Bearer {TOKEN}

加入前會回傳401

加入後就能成功回傳資料!


這樣API就有基本的驗證機制了,但還有許多需要調整部分,例如Token強制失效或是加入Role權限...等等。
這些待後續有需求後再來補充。

參考文章:

如何在 ASP.NET Core 6 使用 Token-based 身份認證與授權 (JWT)










[React]mui導覽列&路由設定

 

React的概念以及學習可直接前往官網,官網有很棒的學習指南只要跟著步驟就可輕鬆入門。

這篇主要是示範如何使用mui和Router開發出可切頁的網站,範例程式碼可參考這裡


先在clientapp/src/pages新增兩個頁面,分別是Home.tsx和About.tsx

//Home.tsx
-----------------------------------------------------------
export function Home() {
    return (
        <>
            <h1>Home Page</h1>
            <p>Welcome!</p>
        </>
    );
}
-----------------------------------------------------------
//About.tsx
-----------------------------------------------------------
export function About() {
    return (
        <>
            <h1>About Page</h1>
            <p>Welcome!</p>
        </>
    );
}
-----------------------------------------------------------

再開啟clientapp/src/app.tsx,改成下面的程式碼。

//App.tsx
-----------------------------------------------------------
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Routes } from "react-router-dom";
import { About } from "./pages/About";
import { Home } from "./pages/Home";
import Box from "@mui/material/Box";

function App() {
    return (
        <Router>
            <Box sx={{ display: "flex" }}>
                <Box component="main" sx={{ flexGrow: 1, p: 3 }}>
                    <Routes>
                        {/* pages */}
                        <Route path="/" element={<Home />} />
                        <Route path="/About" element={<About />} />
                    </Routes>
                </Box>
            </Box>
        </Router>
    );
}

export default App;
-----------------------------------------------------------

這樣一個有路由的網站就做好了!可以跑跑看並且網址加上Home或About看看是否可以正確換頁。



說明一下剛加入的程式碼,Home.tsx和About.tsx是使用React Function Component實作,方法只要return JSX格式就可以顯示畫面。

而App.tsx內使用react-routre-dom來實現路由,先用BrowserRouter包住Routes,而Routes內就會註冊各頁面的路由模式,這邊使用最基本的path="/About"導向element的About頁面。(也有更進階的路由設定,但目前沒用到就暫不說明)

//Route撰寫範例
-----------------------------------------------------------
<Route path="/About" element={<About />} />
-----------------------------------------------------------

p.s. react-router-dom的路由方式是有順序性的比對,從第一個只要有比對到就會顯示該Elementm


頁面跟路由都完成了,接下來現在就來寫導覽列,先用最簡單的超連結方式去實現

在App.tsx內加入下方紅字部分

//App.tsx
-----------------------------------------------------------
import { BrowserRouter as Router, Route,Routes  } from "react-router-dom";
import { Link} from "react-router-dom";
import { About } from "./pages/About";
import { Home } from "./pages/Home";
import Box from "@mui/material/Box";

function App() {
    return (
        <Router>
            <div>
                <Link to="/">Home</Link>
            </div>
            <div>
                <Link to="/About">About</Link>
            </div>
            <Box sx={{ display: "flex" }}>
                <Box component="main" sx={{ flexGrow: 1, p: 3 }}>
                    <Routes>
                        {/* pages */}
                        <Route path="/" element={<Home />} />
                        <Route path="/About" element={<About />} />
                    </Routes>
                </Box>
            </Box>
        </Router>
    );
}

export default App;
-----------------------------------------------------------

切換的連結就完成,點點看是否能夠成功切頁。



簡易的切頁完成了,再來就使用mui的Appbar實作漂亮的導覽頁。

先連到mui的appbar說明頁->傳送門

再來可以選擇自己喜歡的長相,範例使用Basic App bar


點選<>後再點選左側的TS,就會顯示程式碼
將程式碼微調之後寫入App.tsx,如下方紅字

//App.tsx
-----------------------------------------------------------
import { BrowserRouter as Router, Route,Routes  } from "react-router-dom";
import { Link} from "react-router-dom";
import { About } from "./pages/About";
import { Home } from "./pages/Home";
import Box from "@mui/material/Box";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import MenuIcon from '@mui/icons-material/Menu';

function App() {
    return (
        <Router>
            <Box sx={{ display: "flex" }}>
             <AppBar position="fixed">
                    <Toolbar>
                        <IconButton
                            size="large"
                            edge="start"
                            color="inherit"
                            aria-label="menu"
                            sx={{ mr: 2 }}
                        >
                            <MenuIcon />
                        </IconButton>
                        <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
                            FirstReact
                        </Typography>
                        <Button color="inherit">
                            <Link to="/" style={{
                                color: "white"
                            }}>Home</Link>
                        </Button>
                        <Button color="inherit">
                            <Link to="/About" style={{
                                color: "white"
                            }}>About</Link>
                        </Button>
                    </Toolbar>
                </AppBar>
                <Box component="main" sx={{ flexGrow: 1, p: 3, pt: 5 }}>
                    <Routes>
                        {/* pages */}
                        <Route path="/" element={<Home />} />
                        <Route path="/About" element={<About />} />
                    </Routes>
                </Box>
            </Box>
        </Router>
    );
}

export default App;
-----------------------------------------------------------

這樣就有漂亮的導覽列了!馬上開啟偵錯看看效果如何



這樣做完就已經是一個完整的網頁,在美化一下就可以上線了!(想太多,連API都還沒串就想上線!)


下一篇就是要串接API去實作登入機制,也可以。








【.Net Core】 EF Core + Web API 實作

 EF Core是Entity Framework在.Net Core使用的版本,功能幾乎相同,但具有輕巧、高擴充性以及高效能等優點,建議各位學習。 通常在.Net Core如果要用ORM的方式會有兩種選擇分別是EF Core以及Dapper。 從其他網路文章可以看到這兩種在最新...