asp.net core8 mvc에서 jwt토큰로그인 관련 질문입니다.

1/25 수정글

GitHub - kr191201/asp.net-core8
깃헙 생성하여 프로젝트 업로드 해 두었습니다.
LoginController.cs 위주로 봐주시면 됩니다.
이슈 페이지에 내용을 정리해 두었습니다.
감사합니다!

읽어주셔서 감사합니다.
혹시 읽어보시고 추가 내용이 더 필요하다 싶으시면 댓글로 알려주세요.

제가 asp.net core8을 공부한지 2개월 정도 된 것 같은데요
백지인 상태에서 jwt로그인을 구현하려다보니 막히네요.
계속 이것저것 AI한테 물어가며 수정은 해봤는데… 도저히 진도가 안 나가는 상태입니다.
도움주시면 정말 감사하겠습니다…


asp.net core8 mv

c에서 jwt로 로그인을 구현하고 있는데
막혀서 물어볼 곳이 없어서 이곳에 질문글을 작성해 보는데요.
적는다고 적었지만 빠졌거나 설명이 이상한 부분이 있다면 말씀 부탁드립니다.

현재 상황은요
로그인 버튼을 누르면 토큰값을 로컬 스토리지에 저장하고 있습니다.
그후에 사용자 정보를 불러오는 부분을 곧바로 실행하는데요
클라이언트에서 fetch로 /Login/GetUserInfo를 헤더-토큰값을 보낸뒤라서 그런지 해당 액션메서드에서는 if (User.Identity.IsAuthenticated) { 값이 true로 잘 찍히고 있습니다.

다만 로그인후에 메인페이지나 다른 페이지를 수동으로 url입력해서 실행해 보면 공통파일인 /_Nav.cshtml에서는 if (User.Identity.IsAuthenticated) { 부분이 false로 찍히고 있습니다.

제가 알고 있기로는 JWT토큰 설정을 program.cs에서 하면 검증을 통해서 User객체에 데이터가 만들어지고 이걸 기반으로 사용하면 되는 것으로 이해했거든요
근데 막상 /_Nav.cshtml에서 사용하려니 false라서 이유를 못찾겠더라고요…

Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

IConfiguration configuration = builder.Configuration;

/*

  • JWT 기반 인증 설정
  • ASP.NET Core 애플리케이션에서 JWT(JSON Web Token) 인증을 설정하는 역할을 합니다.
  • 그리고 TokenValidationParameters 속성을 통해
  • 전달되는 값들은 JWT 토큰의 유효성을 검증하는 데 사용되는 중요한 정보들을 담고 있습니다.

*/
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// 추출된 토큰에 대해 아래에서 설정된 항목들에 따라 유효성 검증을 수행합니다.
// 검증 성공 시 HttpContext.User에 사용자 정보가 설정되고, 이후 코드에서 User.Identity.IsAuthenticated를 통해 사용자 인증 여부를 확인할 수 있습니다.
// 검증 실패 시 검증에 실패하면 인증 실패 처리를 수행합니다.
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 토큰을 발급한 서버(issuer)가 올바른지 확인합니다. 즉, 설정된 ValidIssuer 값과 일치하는지 비교합니다.
ValidateAudience = true, // 토큰이 발급된 대상(audience)이 올바른지 확인합니다. 즉, 설정된 ValidAudience 값과 일치하는지 비교합니다.
ValidateLifetime = true, // 토큰의 유효 기간을 검사합니다. 발급된 시간과 만료 시간을 비교하여 현재 시간이 유효 기간 내에 있는지 확인합니다.
ValidateIssuerSigningKey = true, // 토큰 서명의 유효성을 검사합니다. 즉, 설정된 IssuerSigningKey를 사용하여 토큰 서명을 검증합니다.

    // 아래 3가지
    // 각각 토큰 발급 서버, 토큰 사용 대상, 토큰 서명에 사용된 비밀키를 나타내는 값입니다. 이 값들은 JWT 토큰 생성 시에도 사용되어 토큰에 포함됩니다.
    ValidIssuer = configuration["Jwt:Issuer"], 
    ValidAudience = configuration["Jwt:Audience"],
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])) 
};

})
//.AddCookie(options =>
//{
// options.Cookie.SameSite = SameSiteMode.Strict; // SameSite 속성 설정
// options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS에서만 쿠키 전송

//})
;

builder.Services.AddControllersWithViews();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

// 요청 처리 파이프라인 설정
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(“/Home/Error”);
app.UseHsts();
} else
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication(); // 첫번째 호출, 인증이 먼저 이루어진 후에
app.UseAuthorization(); // 두번째 호출, 권한 부여가 진행되어야 함
app.MapControllers();

app.MapRazorPages();
app.UseEndpoints(endpoints => // 4 추가
{
endpoints.MapControllers();
});

app.MapControllerRoute(
name: “default”,
pattern: “{controller=Login}/{action=Index}/{id?}”);

app.Run();
로 설정해 둔 상태입니다.

  1. 로그인 버튼 클릭시 실행되는 자바스크립트 함수(클라이언트단)

async function fn_loginProc() {
var USEREMAIL = ("#USEREMAIL").val(); var USERPASSWORD = (“#USERPASSWORD”).val();

const response = await fetch('https://localhost:7123/Login/proc', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ USEREMAIL: USEREMAIL, USERPASSWORD: USERPASSWORD })
});

if (response.ok) {
    const data = await response.json();
    localStorage.setItem('jwt', data.token); // JWT 토큰 저장
   
    fn_GetUserInfo();
    
} else {
    console.error('Login failed');
}

}

  1. 로그인 실행 로직(서버단)

[HttpPost]
public IActionResult proc([FromBody] LoginModel model)
{
try
{
_connection.Open();

    var parameters = new DynamicParameters();
    parameters.Add("@USEREMAIL", model.USEREMAIL, DbType.String, ParameterDirection.Input);
    parameters.Add("@USERPASSWORD", model.USERPASSWORD, DbType.String, ParameterDirection.Input);
    parameters.Add("@Result", dbType: DbType.Int32, direction: ParameterDirection.Output);

    _connection.Execute("USP_LOGIN", parameters, commandType: CommandType.StoredProcedure); // 프로시저 실행

	int result = parameters.Get<int>("@Result"); // OUTPUT 매개변수 값 읽기

	if (result == 1)
    {
        var token = GenerateToken(model);
        return Ok(new { Token = token });
    }
    else
    {
		return Unauthorized(new { Error = "Invalid email or password." }); // 인증 실패 시
	}
}
catch (Exception ex)
{
    // 예외 처리 (로그 기록, 사용자에게 에러 메시지 등)
    return StatusCode(500, "An error occurred while processing your request.");
}
finally
{
    if (_connection.State == ConnectionState.Open)
    {
        _connection.Close();
    }
}

}

  1. 로그인 후 사용자 정보 불러오는 부분(클라이언트단)

function fn_GetUserInfo() {

const token = localStorage.getItem('jwt');

await fetch('https://localhost:7123/Login/GetUserInfo', {
    method: 'GET',
    headers: {
        'Authorization': `Bearer ${localStorage.getItem('jwt')}`
    }
})
.then(response => response.json())
.then(data => {
    // 서버에서 받은 데이터 처리
    console.log(data); // 받은 데이터 콘솔에 출력
})
.catch(error => {
    // 에러 처리
    console.error('Error:', error);
    // 사용자에게 에러 메시지 표시
    alert('데이터를 가져오는 중 오류가 발생했습니다.');
});

}

  1. 사용자 정보 불러오는 부분(서버단)
    이곳에서는 if (User.Identity.IsAuthenticated) { 부분이 true로 떨어집니다
    다만 다음 공통파일인 /_Nav.cshtml에서는 로그인상태인지 아닌지를 표시하는 부분에서 사용중인데 false로 떨어집니다

program.cs에서 설정된 부분이 자동 검증해줘서 User객체에 값을 넣어놓는다고 알고 있는데
그러면 모든 곳에서 if (User.Identity.IsAuthenticated) { 이렇게 사용할수가 있는걸로 이해하고 있거든요
근데 false로 떨어집니다.

[HttpGet]
public IActionResult GetUserInfo()
{
if (User.Identity.IsAuthenticated) {
// 1. 사용자 ID 추출
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;

    var userName = User.Identity.Name;

    _connection.Open();
    var parameters = new DynamicParameters();

    parameters.Add("@SEL_USER_EMAIL", userEmail);

    var result = _connection.Query<TblUserList>("USP_USER_INFO", parameters, commandType: CommandType.StoredProcedure).ToList();

    return Ok(new { IsAuthenticated = true, UserName = userName, UserEmail = userEmail }); // 인증 상태와 사용자 정보를 함께 반환
} else {
    return Ok(new { IsAuthenticated = false }); // 인증되지 않은 경우에도 JSON 반환
}
//return View();

}

3 Likes

Discourse는 마크다운 문법을 사용하여 글을 작성하도록 되어있습니다. 읽기에 큰 지장은 없는 것 같습니다만, 다음 번에 글을 올려주실 때에는 마크다운 문법을 준수하여 보기에 이상이 없는지 검토한 후 올려주시면 감사하겠습니다.

2 Likes

내용이 정리가 안 돼서 정확한 지는 모르겠지만, 클라이언트 단에서 JWT 를 보내는 코드가 없네요?

코드가 잘 돌아 가는지 확인하려면 쿠키 인증을 지우고 하세요.

현재 JWT 와 쿠키 인증이 모두 돌아 가고 있어서, 다시 말하면, 쿠키도 발급되고, JWT 도 발급되기 때문에, HttpContext.User 만으로는 코드가 정상 동작하는 지 확인하는 것에 한계가 있습니다.

2 Likes

말씀해주신 코드상으로는 특별히 이상한 부분은 없는것 같긴한데, 전체를 확인하기에는 누락된 부분도 많은 것 같습니다
샘플코드를 정리해서 github과 같은 소스저장소를 통해 공유해주시면 좋을 것 같습니다

닷넷 인증 관련해서 레퍼런스로 자주 보는 블로그 추천 드립니다.

2 Likes

안녕하세요!
말씀해 주셔서 깃헙을 생성하여 프로젝트 업로드를 해보았습니다.
공유드릴게요.

감사합니다!

1 Like

좋은 정보 공유해 주셔서 감사합니다!

1 Like

네 감사합니다.
마크다운 숙지하겠습니다!

2 Likes

위 정황으로만 본다면, 수동으로 URL 입력하여 네비게이션을 하면 그 요청에는 JWT 가 포함되지 않으니까 인증이 실패하는 게 정상입니다.

UserInfo 에는 JWT 를 포함시켰으니까 정상 인증이 된 것과 대비되는 것이죠.

수동 네비게이션 시에도 JWT 인증이 가능하게 만드려면, JWT를 쿠키에 저장해야 하는데, 보안 상 문제가 많은 방식입니다.

1 Like

안녕하세요.
바쁘신 와중에도 도움주셔서 감사합니다.

JWT인증이 필요한 부분은 헤더에 ‘Authorization’: Bearer ${token} 를 포함하여 api요청을 하는데. 이 경우에는 해당 액션메서드에서 if (User.Identity.IsAuthenticated) { true로 실행이 되거든요.

근데 저의 경우에는 /_nav.cshtml에서는 false인 것이고요.
제가 알고 있는 내용으로는
JWT설정을 program.cs에서 해주고 asp.net core가 자동으로 토큰을 검증해서 User객체에 데이터들을 할당한다고 알고 있거든요

그렇다면… 제 생각에는 /_nav.cshtml에서도 true로 처리가 될 것 같았는데
잘못 이해하고 있는걸까요? 그러면… 수동으로 /home/index를 접속해도 공통 파일인 _nav.cshtml에서도 true가 되겠구나 싶었습니다… 음 아직 개념이 덜 잡혔는지…제가 부족해도 이해 부탁드리겠습니다…꾸벅

유효한 JWT 가 요청에 포함되어 있다면, 언제나 true 가 됩니다.

그런데, 브라우저 주소창에 수동으로 주소를 입력하면, 브라우저가 해당 주소로 Get 요청을 보내는데, 그 요청에는 JWT 가 (자동으로) 포함되지 않겠죠?

요청에 포함되지 않았으니, 검증할 JWT도 없는 것입니다.

조심스럽게 드는 생각은, 네비게이션과 fetch 를 혼동하고 계신 것 같습니다.
JWT를 포함시킨 fetch 요청을 /home/index 에 보내면, true 로 설정되는 것을 확인할 수 있을 것입니다.

3 Likes

BigSquare 님이 말씀하신대로인데 나름의 경험으로 정리해드리자면,

  1. JWT 인증 후 fetch 시에는 직접 header 에 Authroization 을 추가된 상태이므로 인증이 완료된 상태입니다.
  2. 브라우저에서 직접 페이지 이동시에는 header 에 해당 내용이 없으니 인증이 되지 않은 상태입니다.
  3. 브라우저 페이지 이동 등을 통해서 쿠키에 있는 인증토큰을 자동으로 꺼내오게 하고 싶다면, AddJwtBearer 의 options의 Event 추가를 통해서 MessageReceived 시에 쿠키에서 토큰을 가져올수 있도록 추가해줘야합니다.

추가로 첨언하자면, 코드를 첨부해주실때는 최소한 실행 가능한 상태로 올려주셔야합니다.
답변자가 소스를 동작 가능한 상태로 수정해서 원하시는 부분을 확인 후 답변드리는 건 컨설팅의 영역에 가깝기 때문에 답변을 얻기가 더 어려워집니다.

4 Likes

fetch로 헤더에 토큰값 넣어서 요청주고 처리하는 부분에서 좀 헤맸었는데요.
페이지 이동시키고 이동된 페이지에서 인증여부 체크가 잘 안 됐던거라서요.
여러분들이 작성해 주신 댓글들로 갈피를 잡게 된 것 같습니다ㅜㅜ
매번 헤더에 토큰값을 넣어서 요청을 해야하나 싶었는데 그건 또 아니고 말씀주신 쿠키인증을 쓰면 될 것 같은데요. 제가 좀 더 찾아보고 작업해 보겠습니다!
그리고 첨언 주셔서 감사합니다.
깃헙에 .sln 파일도 올려서 업데이트하겠습니다 ㅎㅎ

모두 즐겁고 건강한 설연휴 보내시기를요!

안녕하세요.
말씀해 주신 내용 참고하여 아래와 같이 처리하였더니 로그인 상태일 경우에는 로그아웃으로 표시, 미로그인 상태일 경우에는 로그인으로 표시를 할 수 있게 되었습니다.

  1. 토큰값을 쿠키로 저장
  2. program.cs에서 AddJwtBearer 부분에 이벤트를 추가하여
    네비게이션바 파일에서 @if (User.Identity.IsAuthenticated) 체크하여 처리

감사합니다.

작업을 하면서 궁금한 것이 생겼는데요.
JWT를 쿠키에 저장해서 저처럼 사용하는 방법이 일반적인 방법인지 궁금하더라고요…
제가 잘못된? 방향으로 처리하고 있는건지도 급 혼란스럽고 해서 조심스럽게 여쭤봅니다…

1 Like

일반적인 방법입니다
설계에 따라서 달라질수는 있는데 일반적으로 접근성이 제일 좋다보니 쿠키에 많이 넣습니다

1 Like

말씀 감사합니다
감기 조심하세요!