상세 컨텐츠

본문 제목

[ASP.NET Core Web API] JWT 인증

.NET/ASP.NET Core

by 클리엘 클리엘 2021. 3. 23. 10:40

본문

728x90

JWT(JSON Web Token)는 전통적인 폼 로그인 방식이 아닌 Token이라는 일련의 암호화된 문자열을 통해서 클라이언트와 서버 간의 인증을 처리하는 방식입니다. ASP.NET Core를 통해 Web API 구현할 때도 JWT를 통한 인증방식을 구현할 수 있습니다.

 

우선 Web API 프로젝트를 생성하고 startup.cs의 ConfigureServices 메서드에 아래와 같은 코드를 추가해 JWT 인증 스키마를 등록합니다.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = Configuration["Jwt:Issuer"],
        ValidAudience = Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])),
        ClockSkew = TimeSpan.Zero
    };
});

ValidateIssuer는 Issuer의 유효성여부를 ValidAudience는 Audiance의 유효성 여부, ValidateLifetime는 Token의 생명주기를, ValidateIssuerSigningKey는 Token의 유효성을 검증할지 설정합니다.

 

ValidIssuer는 Token의 발행자를 ValidAudience는 청중(Token을 받을 대상)을 지정하는데 특별한 경우가 아니면 JWT인증을 수행하는 도메인을 지정합니다. 마지막으로 IssuerSigningKey는 Token을 발행할 암호화 키를 지정하며 ClockSkew는 시간을 확인할 때 적용할 클럭 스큐 단위 시간을 0부터 60사이의 값으로 설정합니다.

 

위 설정에 보면 ValidIssuer와 ValidAudience, IssuerSigningKey은 직접 값을 설정하지 않고 설정된 내용으로부터 가져오도록 하였습니다. 값을 그대로 하드 코딩해도 상관은 없지만 이들 값은 중요하기에 따로 설정을 통해 관리하는 것을 권합니다.

 

설정은 appsetting.json 파일에 다음과 같이 키와 값으로 등록할 수 있습니다.

"Jwt": {
  "SecretKey": "abcdefghijklmnopqrstuvwxyz1234567890",
  "Issuer": "http://api.cliel.com/",
  "Audience": "http://api.cliel.com/"
}

 

 

설정값중 특히 SecretKey부분은 외부에 노출되지 않도록 주의해야 합니다.

 

다음으로 startup.cs의 Configure() 메서드를 수정해 애플리케이션에서 인증서비스를 제공할 수 있도록 인증 관련 메서드를 추가합니다.

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

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

 

그리고 인증을 처리하기 위한 인증정책을 정의하는 클래스를 생성합니다.

public class Policies
{
    public const string Admin = "Admin";
    public const string User = "User";

    public static AuthorizationPolicy AdminPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole(Admin).Build();
    }

    public static AuthorizationPolicy UserPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole(User).Build();
    }
}

위에서 작성된 클래스에 의해 인증은 Admin과 User 2개의 그룹으로 인증처리를 관리할 것입니다. 다시 startup.cs로 돌아가 ConfigureService() 메서드에서 위에서 정의한 정책이 적용될 수 있도록 처리합니다.

services.AddAuthorization(config =>
{
	config.AddPolicy(Policies.Admin, Policies.AdminPolicy());
	config.AddPolicy(Policies.User, Policies.UserPolicy());
});

services.AddControllers();

이제 인증을 적용하기 위한 사용자 모델 클래스를 추가합니다. 이 클래스는 직접 작성될 수도 있고 DB의 모델을 따라갈 수도 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Models
{
  public class User
  {
    public string UserName { get; set; }
    public string Password { get; set; }
    public string UserRole { get; set; }
  }
}

마지막으로 로그인 컨트롤러를 생성해 위에서 작성한 JWT인증을 적용할 수 있도록 합니다.

[HttpPost]
[AllowAnonymous]
public IActionResult Login([FromBody] User login)
{
    IActionResult response = Unauthorized();

    User user = AuthenticateUser(login);
    if (user != null)
    {
        var tokenString = GenerateJWTToken(user);
        response = Ok(new
        {
            token = tokenString,
            userDetails = user,
        });
    }
    return response;
}

private User AuthenticateUser(User loginCredentials)
{
    User user = appUsers.SingleOrDefault(x => x.UserName == loginCredentials.UserName && x.Password == loginCredentials.Password);
    return user;
}

private string GenerateJWTToken(User userInfo)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.Sha512);
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserName),
        new Claim("role",userInfo.UserRole),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    };

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.Now.AddHours(1),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

먼저 Login으로 사용자의 이름과 패스워드가 넘어오면 이를 AuthenicateUser() 메서드를 통해 실제 해당 사용자가 있는지를 확인합니다. 사용자가 존재하면 GenerateJWTToken() 메서드를 통해 해당 사용자에게 토큰을 발급하여 인증을 마무리하게 됩니다. 토큰은 설정된 암호화 키와 지정된 암호화 방식을 사용하여 생성됩니다.

 

키가 너무 짧거나 암호화방식이 제대로 지정되지 않으면 오류가 발생하므로 주의해야 합니다.

 

issuer과 audience, SecretKey 등은 설정 파일에서 가져오도록 한 것인데 이 객체는 생성자에서 인터페이스로 전달받는 의존성 주입을 통해 구현합니다.

private readonly IConfiguration _config;

public LoginController(IConfiguration config)
{
    _config = config;
}

이제 인증이 필요한 API에 아래와 같이 Authorize 특성을 추가하면 인증한 사용자만이 API로 접근할 수 있게 됩니다.

[HttpGet]
[Route("test")]
[Authorize]
public string Get()
{
    return User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

[HttpGet]
[Route("User")]
[Authorize(Policy = Policies.User)]
public string Get1()
{
    return User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

참고로 GenerateJWTToken() 메서드를 보면 사용자에게 Token을 발급할때 사용자의 정보를 Claim 객체를 통해 저장하고 있는데 Clanim을 사용하면 역으로 설정된 사용자 정보를 가져올 수도 있습니다.

[HttpGet]
[AllowAnonymous]
public ObjectResult Get0()
{
    var currentUser = HttpContext.User;

    if (currentUser.HasClaim(c => c.Type == "role"))
    {
        return StatusCode(Microsoft.AspNetCore.Http.StatusCodes.Status200OK, currentUser.Claims.FirstOrDefault(c => c.Type == "role").Value);
    }
    else
    {
        return StatusCode(Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized, "My error message");
    }
}

클라이언트에서는 Login API를 호출하여 인증토큰을 발급받고

 

인증이 필요한 API를 호출합니다.

 

728x90

관련글 더보기

댓글 영역