feat: JWT 인증 모듈 구현 (#20)
Some checks failed
SPMS_API/pipeline/head There was a failure building this commit
Some checks failed
SPMS_API/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/21
This commit is contained in:
commit
f3714ff8fb
55
SPMS.API/Extensions/AuthenticationExtensions.cs
Normal file
55
SPMS.API/Extensions/AuthenticationExtensions.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SPMS.Application.Settings;
|
||||
|
||||
namespace SPMS.API.Extensions;
|
||||
|
||||
public static class AuthenticationExtensions
|
||||
{
|
||||
public static IServiceCollection AddJwtAuthentication(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var jwtSettings = configuration.GetSection(JwtSettings.SectionName).Get<JwtSettings>()!;
|
||||
|
||||
services.Configure<JwtSettings>(configuration.GetSection(JwtSettings.SectionName));
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtSettings.Issuer,
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAuthorizationPolicies(this IServiceCollection services)
|
||||
{
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("SuperOnly", policy =>
|
||||
policy.RequireRole("Super"));
|
||||
|
||||
options.AddPolicy("ManagerOrAbove", policy =>
|
||||
policy.RequireRole("Super", "Manager"));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using SPMS.API.Extensions;
|
||||
using SPMS.API.Middlewares;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Interfaces;
|
||||
using SPMS.Infrastructure;
|
||||
using SPMS.Infrastructure.Auth;
|
||||
using SPMS.Infrastructure.Persistence;
|
||||
using SPMS.Infrastructure.Persistence.Repositories;
|
||||
|
||||
|
|
@ -22,6 +25,11 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|||
|
||||
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||
|
||||
// JWT 인증/인가
|
||||
builder.Services.AddJwtAuthentication(builder.Configuration);
|
||||
builder.Services.AddAuthorizationPolicies();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
@ -34,11 +42,11 @@ if (app.Environment.IsDevelopment())
|
|||
app.MapOpenApi();
|
||||
}
|
||||
var webRoot = app.Environment.WebRootPath;
|
||||
Console.WriteLine($"[System] Web Root Path: {webRoot}"); // 로그에 경로 찍어보기
|
||||
Console.WriteLine($"[System] Web Root Path: {webRoot}");
|
||||
|
||||
if (Directory.Exists(webRoot))
|
||||
{
|
||||
app.UseStaticFiles(); // 경로가 있으면 파일 서빙
|
||||
app.UseStaticFiles();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -48,7 +56,13 @@ else
|
|||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
|
||||
// [4] 요청 처리
|
||||
// ── 10. JWT 인증 ──
|
||||
app.UseAuthentication();
|
||||
|
||||
// ── 11. 역할 인가 ──
|
||||
app.UseAuthorization();
|
||||
|
||||
// [엔드포인트 매핑]
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
"SecretKey": "",
|
||||
"Issuer": "SPMS",
|
||||
"Audience": "SPMS",
|
||||
"ExpiryMinutes": 10
|
||||
"ExpiryMinutes": 10,
|
||||
"RefreshTokenExpiryDays": 7
|
||||
},
|
||||
"RabbitMQ": {
|
||||
"HostName": "",
|
||||
|
|
|
|||
10
SPMS.Application/Interfaces/IJwtService.cs
Normal file
10
SPMS.Application/Interfaces/IJwtService.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using System.Security.Claims;
|
||||
|
||||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
string GenerateAccessToken(long adminId, string role, string? serviceCode = null);
|
||||
ClaimsPrincipal? ValidateAccessToken(string token);
|
||||
string GenerateRefreshToken();
|
||||
}
|
||||
12
SPMS.Application/Settings/JwtSettings.cs
Normal file
12
SPMS.Application/Settings/JwtSettings.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace SPMS.Application.Settings;
|
||||
|
||||
public class JwtSettings
|
||||
{
|
||||
public const string SectionName = "JwtSettings";
|
||||
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
public string Issuer { get; set; } = string.Empty;
|
||||
public string Audience { get; set; } = string.Empty;
|
||||
public int ExpiryMinutes { get; set; } = 60;
|
||||
public int RefreshTokenExpiryDays { get; set; } = 7;
|
||||
}
|
||||
80
SPMS.Infrastructure/Auth/JwtService.cs
Normal file
80
SPMS.Infrastructure/Auth/JwtService.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Application.Settings;
|
||||
|
||||
namespace SPMS.Infrastructure.Auth;
|
||||
|
||||
public class JwtService : IJwtService
|
||||
{
|
||||
private readonly JwtSettings _settings;
|
||||
|
||||
public JwtService(IOptions<JwtSettings> settings)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
public string GenerateAccessToken(long adminId, string role, string? serviceCode = null)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, adminId.ToString()),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
|
||||
new(ClaimTypes.Role, role)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(serviceCode))
|
||||
claims.Add(new Claim("ServiceCode", serviceCode));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _settings.Issuer,
|
||||
audience: _settings.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(_settings.ExpiryMinutes),
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal? ValidateAccessToken(string token)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
|
||||
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = _settings.Issuer,
|
||||
ValidAudience = _settings.Audience,
|
||||
IssuerSigningKey = key,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
return handler.ValidateToken(token, validationParameters, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string GenerateRefreshToken()
|
||||
{
|
||||
var randomBytes = new byte[64];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(randomBytes);
|
||||
return Convert.ToBase64String(randomBytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.2" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user