diff --git a/API註解操作說明(必看).txt b/API註解操作說明(必看).txt new file mode 100644 index 0000000..e09ee74 --- /dev/null +++ b/API註解操作說明(必看).txt @@ -0,0 +1,12 @@ +JWTdemo資料夾→JWTdemo.csproj 開啟 + + + net6.0 + enable + enable //新增這兩行 + true //新增這兩行 + $(NoWarn);1591 + + +1.true:這個設定告訴編譯器生成 XML 註解檔案。當您的專案編譯時,它會將 XML 註解嵌入到組件中,以供 Swagger 或其他工具使用。 +2.$(NoWarn);1591:這個設定用來抑制編譯器警告 1591。警告 1591 是指程式碼中的缺少 XML 註解的警告。這裡的設定的意思是告訴編譯器忽略這個特定的警告,因為您已經啟用了 XML 註解生成,而不希望因缺少註解而收到警告。 \ No newline at end of file diff --git a/JWTdemo.sln b/JWTdemo.sln new file mode 100644 index 0000000..31305fa --- /dev/null +++ b/JWTdemo.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34031.279 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JWTdemo", "JWTdemo\JWTdemo.csproj", "{4C54D743-8EE0-44C9-8C9D-010A306C4AE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4C54D743-8EE0-44C9-8C9D-010A306C4AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C54D743-8EE0-44C9-8C9D-010A306C4AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C54D743-8EE0-44C9-8C9D-010A306C4AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C54D743-8EE0-44C9-8C9D-010A306C4AE7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5537568D-28F4-41EA-A9EE-89BAF86E4808} + EndGlobalSection +EndGlobal diff --git a/JWTdemo/Authorization/AllowAnonymousAttribute.cs b/JWTdemo/Authorization/AllowAnonymousAttribute.cs new file mode 100644 index 0000000..4d0b894 --- /dev/null +++ b/JWTdemo/Authorization/AllowAnonymousAttribute.cs @@ -0,0 +1,6 @@ +namespace JWTdemo.Authorization; + +[AttributeUsage(AttributeTargets.Method)] +public class AllowAnonymousAttribute : Attribute +{ +} \ No newline at end of file diff --git a/JWTdemo/Authorization/AuthorizeAttribute.cs b/JWTdemo/Authorization/AuthorizeAttribute.cs new file mode 100644 index 0000000..88e34e6 --- /dev/null +++ b/JWTdemo/Authorization/AuthorizeAttribute.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; +using JWTdemo.Entities; + +namespace JWTdemo.Authorization; +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AuthorizeAttribute : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + // skip authorization if action is decorated with [AllowAnonymous] attribute + var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any(); + if (allowAnonymous) + return; + + // authorization + var user = (User?)context.HttpContext.Items["User"]; + if (user == null) + { + // not logged in or role not authorized + context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; + } + } +} \ No newline at end of file diff --git a/JWTdemo/Authorization/JwtMiddleware.cs b/JWTdemo/Authorization/JwtMiddleware.cs new file mode 100644 index 0000000..a25ab73 --- /dev/null +++ b/JWTdemo/Authorization/JwtMiddleware.cs @@ -0,0 +1,23 @@ +namespace JWTdemo.Authorization; +public class JwtMiddleware +{ + private readonly RequestDelegate _next; + + public JwtMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils) + { + var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + var userId = jwtUtils.ValidateJwtToken(token); + if (userId != null) + { + // attach user to context on successful jwt validation + context.Items["User"] = userService.GetById(userId.Value); + } + //var stop = "1"; + await _next(context); + } +} \ No newline at end of file diff --git a/JWTdemo/Authorization/JwtUtils.cs b/JWTdemo/Authorization/JwtUtils.cs new file mode 100644 index 0000000..74735e9 --- /dev/null +++ b/JWTdemo/Authorization/JwtUtils.cs @@ -0,0 +1,106 @@ +namespace JWTdemo.Authorization; + +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using JWTdemo.Entities; +using JWTdemo.Helpers; + + +public interface IJwtUtils +{ + public string GenerateJwtToken(User user); + public int? ValidateJwtToken(string? token); +} + +public class JwtUtils : IJwtUtils +{ + private readonly AppSettings _appSettings; + + public JwtUtils(IOptions appSettings) + { + _appSettings = appSettings.Value; + + if (string.IsNullOrEmpty(_appSettings.Secret)) + throw new Exception("JWT secret not configured"); + } + + public string GenerateJwtToken(User user) + { + // generate token that is valid for 7 days + var tokenHandler = new JwtSecurityTokenHandler(); //實例化JWT令牌 + var key = Encoding.ASCII.GetBytes(_appSettings.Secret!); //從配置中獲取應用程序密鑰,用於簽名令牌以確保其完整性和安全性 + var tokenDescriptor = new SecurityTokenDescriptor //定義令牌格式,其包含header(SigningCredentials).payload(expires和subject).signature(簽在header裡面) + { + Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), //payload + Expires = DateTime.UtcNow.AddDays(7), //payload,令牌過期時間 + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) //header + }; + var token = tokenHandler.CreateToken(tokenDescriptor); //創建JWT令牌 + return tokenHandler.WriteToken(token); //將JWT令牌轉為base64編碼 + } + + public int? ValidateJwtToken(string? token) + { + if (token == null) + return null; + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_appSettings.Secret!); + try + { + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) + ClockSkew = TimeSpan.Zero + }, out SecurityToken validatedToken); + + var jwtToken = (JwtSecurityToken)validatedToken; + var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); + + // return user id from JWT token if validation successful + return userId; + } + catch + { + // return null if validation fails + return null; + } + } + + + //0523 + public bool ValidateToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var jwtSecret = "your_jwt_secret"; // JWT 密钥,应与生成令牌时使用的密钥相匹配 + + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero // 设置为零以确保令牌过期时立即失效 + }; + + try + { + SecurityToken validatedToken; + tokenHandler.ValidateToken(token, validationParameters, out validatedToken); + return true; + } + catch + { + return false; + } + } + +} \ No newline at end of file diff --git a/JWTdemo/Controllers/UserController.cs b/JWTdemo/Controllers/UserController.cs new file mode 100644 index 0000000..99669e3 --- /dev/null +++ b/JWTdemo/Controllers/UserController.cs @@ -0,0 +1,28 @@ +using JWTdemo.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using JWTdemo.Models; +using JWTdemo.Authorization; + +namespace JWTdemo.Controllers +{ + [Authorize] //有token才能使用class + [Route("api/[controller]")] + [ApiController] + public class UserController : ControllerBase + { + private readonly SqlContext _context; + public UserController(SqlContext context) + { + _context = context; + } + /// + /// 測試註解 + /// + [HttpGet] + public async Task>> Getuser() + { + return await _context.chatuser.ToListAsync(); + } + } +} diff --git a/JWTdemo/Entities/User.cs b/JWTdemo/Entities/User.cs new file mode 100644 index 0000000..7cf5006 --- /dev/null +++ b/JWTdemo/Entities/User.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace JWTdemo.Entities +{ + public class User + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Username { get; set; } + + [JsonIgnore] //這個就是當有人要get這個資料時,會自動將其隱藏 + public string? Password { get; set; } + } +} diff --git a/JWTdemo/Helpers/AppSettings.cs b/JWTdemo/Helpers/AppSettings.cs new file mode 100644 index 0000000..2fd661c --- /dev/null +++ b/JWTdemo/Helpers/AppSettings.cs @@ -0,0 +1,7 @@ +namespace JWTdemo.Helpers +{ + public class AppSettings + { + public string? Secret { get; set; } + } +} diff --git a/JWTdemo/JWTdemo.csproj b/JWTdemo/JWTdemo.csproj new file mode 100644 index 0000000..24c2170 --- /dev/null +++ b/JWTdemo/JWTdemo.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + diff --git a/JWTdemo/Models/AuthenticateRequest.cs b/JWTdemo/Models/AuthenticateRequest.cs new file mode 100644 index 0000000..96d9389 --- /dev/null +++ b/JWTdemo/Models/AuthenticateRequest.cs @@ -0,0 +1,13 @@ +namespace JWTdemo.Models; + +using System.ComponentModel.DataAnnotations; + + +public class AuthenticateRequest +{ + [Required] + public string? Username { get; set; } + + [Required] + public string? Password { get; set; } +} \ No newline at end of file diff --git a/JWTdemo/Models/AuthenticateResponse.cs b/JWTdemo/Models/AuthenticateResponse.cs new file mode 100644 index 0000000..120d6ee --- /dev/null +++ b/JWTdemo/Models/AuthenticateResponse.cs @@ -0,0 +1,20 @@ +namespace JWTdemo.Models; + +using JWTdemo.Entities; + +public class AuthenticateResponse +{ + public int Id { get; set; } + public string? Name { get; set; } + public string? Username { get; set; } + public string Token { get; set; } + + + public AuthenticateResponse(User user, string token) + { + Id = user.Id; + Name = user.Name; + Username = user.Username; + Token = token; + } +} \ No newline at end of file diff --git a/JWTdemo/Program.cs b/JWTdemo/Program.cs new file mode 100644 index 0000000..84901d6 --- /dev/null +++ b/JWTdemo/Program.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using JWTdemo.Authorization; +using JWTdemo.Helpers; +using System.Configuration; +using System.Reflection; +using JWTdemo.Services; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using Microsoft.OpenApi.Models; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(); +builder.Services.AddControllers(); + +// Add services to the container. +//builder.Services.AddControllersWithViews(); + +//*------------------------------連線PostgreSQL資料庫----------------------------------------------- +var connectionString = "Server=localhost;UserID=postgres;Password=vip125125;Database=postgres;port=5432;"; +builder.Services.AddDbContext(opt => opt.UseNpgsql(connectionString)); + +//*---------------------------------JWT身分驗證------------------------------------------------------- +{ + var services = builder.Services; + services.AddCors(); + services.AddControllers(); + services.Configure(builder.Configuration.GetSection("AppSettings")); + var jwtSettings = builder.Configuration.GetSection("AppSettings").Get(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + //ValidIssuer = "your_issuer", + // ValidAudience = "your_audience", + ClockSkew = TimeSpan.Zero, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)) + }; + }); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApi_data_value", Version = "v1" }); + + // Configure Swagger to use JWT authentication + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + // 将JWT令牌作为所有端点的要求添加到Swagger文档 + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); + }); + + // configure DI for application services + services.AddScoped(); + services.AddScoped(); + // 注册 HttpClient 服务 + services.AddHttpClient(); +} + + +//*---------------------------創專案就有-------------------------- +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +//*---------------------------WebAPI註解設定-------------------------- +builder.Services.AddSwaggerGen(options => +{ + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); +}); +var app = builder.Build(); +//*---------------------------JWT身分驗證------------------------------ +{ + // global cors policy + //在 ASP.NET Core 中啟用 CORS (跨原始來源要求) + // Shows UseCors with CorsPolicyBuilder. + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + // custom jwt auth middleware + app.UseMiddleware(); + + app.MapControllers(); +} + +//-------------------------Swagger初始化------------------------------------- +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "JWTdemo"); //API註解開啟 + }); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/JWTdemo/Properties/launchSettings.json b/JWTdemo/Properties/launchSettings.json new file mode 100644 index 0000000..2580b31 --- /dev/null +++ b/JWTdemo/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:23800", + "sslPort": 44344 + } + }, + "profiles": { + "JWTdemo": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7079;http://localhost:5246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/JWTdemo/Services/SqlContext.cs b/JWTdemo/Services/SqlContext.cs new file mode 100644 index 0000000..e3e7bea --- /dev/null +++ b/JWTdemo/Services/SqlContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using JWTdemo.Entities; + + +namespace JWTdemo.Services +{ + public class SqlContext : DbContext + { + public SqlContext(DbContextOptions options) : base(options) + { + //連接PostgreSQL + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true); + } + public DbSetchatuser { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity().HasKey(o => new { o.Id }); //Primary Key + } + + } +} diff --git a/JWTdemo/Services/UserService.cs b/JWTdemo/Services/UserService.cs new file mode 100644 index 0000000..5798886 --- /dev/null +++ b/JWTdemo/Services/UserService.cs @@ -0,0 +1,70 @@ +using JWTdemo.Authorization; +using JWTdemo.Services; +using JWTdemo.Entities; +using JWTdemo.Models; + +public interface IUserService +{ + AuthenticateResponse? Authenticate(AuthenticateRequest model); + IEnumerable GetAll(); + User? GetById(int id); +} + +public class UserService : IUserService +{ + /* + // users hardcoded for simplicity, store in a db with hashed passwords in production applications + private List user_test = new List + { + new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" }, + new User { Id = 2, FirstName = "Test", LastName = "User", Username = "admin", Password = "admin" } + }; + + public DbSet user_test { get; set; } = null!; + + + public List GetUsers () + { + return _dbContext.user_test.ToList(); + } + + */ + + private readonly IJwtUtils _jwtUtils; + + public UserService(IJwtUtils jwtUtils, SqlContext dbContext) + { + _jwtUtils = jwtUtils; + _dbContext = dbContext; + } + + + private readonly SqlContext _dbContext; + + + public AuthenticateResponse? Authenticate(AuthenticateRequest model) + { + var user = _dbContext.chatuser.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password); + + // return null if user not found + if (user == null) return null; + + // authentication successful so generate jwt token + var token = _jwtUtils.GenerateJwtToken(user); + + return new AuthenticateResponse(user, token); + } + + public IEnumerable GetAll() + { + return _dbContext.chatuser; + } + + public User? GetById(int id) + { + return _dbContext.chatuser.FirstOrDefault(x => x.Id == id); + } + + + +} \ No newline at end of file diff --git a/JWTdemo/appsettings.Development.json b/JWTdemo/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/JWTdemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/JWTdemo/appsettings.json b/JWTdemo/appsettings.json new file mode 100644 index 0000000..f104da0 --- /dev/null +++ b/JWTdemo/appsettings.json @@ -0,0 +1,12 @@ +{ + "AppSettings": { + "Secret": "Leo token test jwt park spaces lab 124" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}