分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
1. 靜態設定 | : | 定義Sqlite資料庫、JWT的憑證 |
2. JWT業務邏輯 | : | 調整JWT產生器的共用方法、增加登出、登入時的註冊 |
3. JWT資料庫邏輯 | : | 對登入、登出時資料庫寫入、更新 |
4. 頁面行為 | : | 登入、登入後的顯示、登出邏輯調整 |
以下是ConstUtil.cs定義了JWT的憑證,因為在系統登入後、登入時都會用到檢核,故抽出便於共用。
public class ConstUtil
{
/// <summary>
/// 發行者
/// </summary>
public const string Issuer = "JwtLoginIssuer";
/// <summary>
/// 加密金鑰
/// </summary>
public const string SignKey = "this_is_a_secure_key_with_length_greater_than_32";
/// <summary>
/// 使用者
/// </summary>
public const string Audience = "JwtLoginAudience";
}
初始化資料庫的方法,這邊是範例用Sqlite舉例,SqlLiteDbUtil.cs
/// <summary>
/// 本地Sqlite DataBase連線
/// </summary>
public static class SqlLiteDbUtil
{
//1. 設定連線配置
public const string DatabaseFileName = @"MyLogin.db";
public const string ConnectionString = "Data Source=" + DatabaseFileName;
static SqlLiteDbUtil()
{
Master = new SQLiteConnection(ConnectionString);
CreateDatabaseIfNotExists();
}
public static IDbConnection Master { get; private set; }
#region 2-1. 資料庫建構
private static void CreateDatabaseIfNotExists()
{
if (!File.Exists(DatabaseFileName))
{
//Create Local Database
Master.Open();
//Initial Tables
CreateDatabase();
}
//2-2. 建立登入表
void CreateDatabase()
{
Master.Execute($@"
CREATE TABLE AccountToken (
AccountName TEXT NOT NULL,
Token TEXT NOT NULL,
IsValid INT NOT NULL,
LastDateTime DATETIME,
PRIMARY KEY (AccountName)
);");
}
#endregion
}
}
主要分成三個區段
1. 登入時的頁面 | : | 登入後,會產生一個新的JWT,確保最新登入者的Token |
2. 登入後的頁面 | : | 登入後,如果在頁面操作,會檢核當前Token是否有效,如果別的地方登入了,目前Token會失效 |
3. 登出時的行為 | : | 登出後,會註銷資料庫最新的Token |
public class JsonWebTokenService
{
private readonly ISqliteRepository _repository;
public JsonWebTokenService(ISqliteRepository repository)
{
_repository = repository;
}
/// <summary>
/// 產生JWT
/// </summary>
public string GenerateToken(LoginModel user,
int id, string type, string nickName)
{
var claims = new[] {
new Claim(ClaimTypes.Name, user.Username),
new Claim("type", type),
new Claim("id", $@"{id}"),
new Claim("nickname", nickName),
};
var jwtKey = Encoding.UTF8.GetBytes(ConstUtil.SignKey);
var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(jwtKey), SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: ConstUtil.Issuer, // 發行者:若解析驗證Token正確性時這個不同會視為驗證失敗
audience: ConstUtil.Audience, // 使用者:若解析驗證Token正確性時這個不同會視為驗證失敗
signingCredentials: signingCredentials,//簽名憑證:若解析驗證Token正確性時這個不同會視為驗證失敗
claims: claims, // 資料:可攜帶用戶資訊,像密碼類的不建議放進,如果被收集過多的token仍有可能被破解
expires: DateTime.UtcNow.AddSeconds(120)//過期時間:如果超過此token會直接報廢
);
//1. 登入時的行為:註冊Token到資料庫中
var newToken = new JwtSecurityTokenHandler().WriteToken(token);
RegistToken(user.Username, newToken);
return newToken;
}
public void RegistToken(string userName, string token)
{
_repository.InsertOrUpdateToken(userName, token);
}
/// <summary>
/// 2. 登入後的行為
/// </summary>
public bool IsMatchToken(string currentToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(currentToken);
//2-1. 檢核時間簽名憑證
var jwtKey = Encoding.UTF8.GetBytes(ConstUtil.SignKey);
tokenHandler.ValidateToken(currentToken, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = ConstUtil.Audience,
ValidIssuer = ConstUtil.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(jwtKey)
}, out var validatedToken);
//2-2. 檢核是否有用戶
var UserName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(UserName))
return false;
//2-3. 檢核資料庫
if (currentToken != _repository.GetToken(UserName))
{
//如果不相同跳出
return false;
}
return true;
}
//3. 登出時的行為
public void SetInValid(string currentToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(currentToken);
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
if (!string.IsNullOrEmpty(userName))
{
_repository.UpdateTokenInValid(userName, currentToken);
}
}
}
主要分成三個區段
1. 紀錄令牌 | : | 登入後,會產生一個新的JWT,紀錄最新登入者對應帳號的Token |
2. 取得令牌 | : | 登入後,如果在頁面操作,取得該帳號的Token提供匹配 |
3. 設成無效令牌 | : | 登出後,會註銷資料庫該帳號的Token |
public class SqliteRepository : ISqliteRepository
{
/// <summary>
/// 1. 紀錄令牌
/// </summary>
public void InsertOrUpdateToken(string account, string token)
{
var sql = $@"
INSERT OR REPLACE INTO AccountToken (AccountName, Token, IsValid, LastDateTime)
VALUES (@AccountName, @Token, @IsValid, datetime('now', 'localtime'))
;";
SqlLiteDbUtil.Master.Execute(sql, new { AccountName = account, IsValid = 1, Token = token });
}
/// <summary>
/// 2. 取得令牌
/// </summary>
public string GetToken(string account)
{
var sql = $@"
SELECT Token
FROM AccountToken
WHERE AccountName = @AccountName
;";
return SqlLiteDbUtil.Master.QueryFirstOrDefault<string>(sql, new { AccountName = account });
}
/// <summary>
/// 3. 設成無效令牌
/// </summary>
public void UpdateTokenInValid(string account, string token)
{
var sql = $@"
UPDATE AccountToken
SET IsValid = @IsValid
WHERE AccountName= @AccountName
AND Token = @Token
;";
SqlLiteDbUtil.Master.Execute(sql, new { AccountName = account, IsValid = 1, Token = token });
}
}
登入頁 Login.razor,與前一版相同。
顯示畫面時會先驗證是否存在Token,若未匹配則視為登出
//1-1. token存在的情況下,檢查是否過期
if (jwtService.IsMatchToken(token))
{
//1-2. 沒有過期則將用戶資料從Token取出,顯示在操作頁面上
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(token);
UserName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
Type = jwtToken.Claims.FirstOrDefault(c => c.Type == "type")?.Value;
Id = jwtToken.Claims.FirstOrDefault(c => c.Type == "id")?.Value;
NickName = jwtToken.Claims.FirstOrDefault(c => c.Type == "nickname")?.Value;
return true;
}
//1-3. token被註銷、或者過期視為不存在
return false;
登出時,強制將該帳號的Token註銷
//3. 登出行為
private void LogoutMethod()
{
// 移除 Session 中的 JWT,導回首頁
HttpContextAccessor.HttpContext.Session.Remove("JWT");
jwtService.SetInValid(HttpContextAccessor?.HttpContext?.Session?.GetString("JWT"));
Navigation.NavigateTo("/");
}
我們需要用同個帳號進行登入 -> 輸入帳號a -> 登入
另一個頁面用無痕開啟(右邊) -> 輸入帳號a -> 登入
無痕頁面(右邊)登入後,兩邊資訊一致
左邊的原始頁面,進行重新整理(F5),可以發現被強制登出
右邊的無痕頁面,進行重新整理(F5),仍保留,這是因為系統只保留最後的JWT