블레이저 사용자 인증 관련 질문드립니다.

안녕하세요. 블레이저를 이용해서 모바일에서 스캔을 하려고 합니다.

사용자에 대한 로그인 기능은 필요가 없어서 메일을 통해 OTP 인증을 하면 인증쿠키 (휘발성)가

발행되고 사이트 이용을 하도록 하고 싶습니다.

(추후 무단 접근을 막기 위해 브라우저 ID도 저장할 예정)

간단히 Test를 진행하고 있는데 HttpClient에서 InvalidOpertionException이 발생하면 쿠키를 발행하지 못하는데 이유가 무엇을까요?? 구현 방법이 잘못되었을까요?

Program.cs

//인증쿠키 JSP와 달리 블레이저는 세션 개념이 없다. 해서 Pricipal이란 쿠키로 세션인증을 거친다.
builder.Services.AddAuthentication("mescookie").AddCookie("mescookie" , options =>
{
    options.Cookie.Name = "mescookie";
    options.LoginPath = "/mes";
    options.Cookie.Path = "/";

    options.SlidingExpiration = false;

});

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpClient();
builder.Services.AddScoped<HttpClient>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

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


app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();


app.MapPost("/issue-cookie", async (HttpContext context, string userId) =>
{
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, userId),
        new Claim(ClaimTypes.Name, userId)
    };

    var identity = new ClaimsIdentity(claims, "mescookie");
    var principal = new ClaimsPrincipal(identity);

    await context.SignInAsync("mescookie", principal, new AuthenticationProperties
    {
        IsPersistent = false   // 세션 쿠키 (브라우저 종료 시 삭제)
    });

    return Results.Ok();
});

app.Run();
접속 레이저 파일 일부

@inject HttpClient Http
@inject NavigationManager Navigation

 private async Task confirmOTP()
 {

     if (string.IsNullOrEmpty(InputOtpCode) || !InputOtpCode!.Equals(OtpCode))
     {
         await DialogService.Alert("유효하지 않는 OTP코드입니다.", "확인!");

     }
     else
     {

         optTimer!.Stop();
         optTimer.Dispose();
         OtpColor = "blue";
         OtpMessage = "인증되었습니다.";

         await Task.Delay(500);

         var result = await DialogService.OpenAsync<SaveInfo>("취급정보저장안내",null,new DialogOptions { Width="550px", Height = "auto" , ShowClose=false});

         if (result)
         {
             //브라우저 고유 ID 불러오는 부분
             //DB 저장
             Console.WriteLine("DB 저장완료!");

         }

         /*인증세션 발급*/
         _ =  OnVerifiedAsync();


     }      

 }

 private async Task OnVerifiedAsync()
    {
        var result = await Http.PostAsJsonAsync("/issue-cookie", new { userId = "otpverified-user" });

        Console.WriteLine(Http is null ? "Null" : "NO");

        if (result.IsSuccessStatusCode)
        {
            // 쿠키 발급됨 → 페이지 새로고침해서 인증된 사용자로 전환
           Navigation.NavigateTo("/counter", forceLoad: true);
        }
    }

현재 OnVerifiedAsync에서 Http 관련 InvalidOpertationException이 발생하는데 이유를 모르겠습니다. Program.cs에서 빌드서비스에 등록을 했고 정상적으로 inject를 했는데 Console에 아무것도 찍히 않는걸 보니 PostAs 여기에서 예외가 발생하는 것 같은데..

  1. 가능하면 Minimal reproduction을 만들어보고 왜 에러가 발생하는지 찾아보시는 것이 좋습니다. 여기서는 Blazor server 프로젝트를 새로 만들고 Post만 만들고 버튼을 클릭하면 Post 요청을 보내는 reproduction 코드에서 에러 찾기가 필요해보이네요.
  2. 사실 이대로 AI한테 물어봐도 좋습니다. Claude와 Gemini 둘 다 “결론부터 말씀드리면, InvalidOperationException이 발생하는 직접적인 원인은 HttpClientBaseAddress가 설정되지 않았기 때문입니다.”라고 하네요.

첫 줄은 IHttpClientFactory 를 주입시켜주고, 두번 째 줄은 HttpClient 를 주입시키는데,

둘다 Base address 설정이 없기 때문에 실제 전송 메서드 호출 시 Full url 을 넘겨 줘야 합니다.

그리고, 둘 중 하나만 설정하는데, 문서에서는 전자가 권고됩니다.

2 Likes

안녕하세요. 지피티한테 몇번을 물어봤는데 계속 엉뚱한 이야기만 해서 여기에 물어봤습니다. BaseAddress가 설정되지 않아 일어난 문제였네요. 감사합니다~!

안녕하세요. BaseAddress가 설정되지 않아 일어난 문제와 제가 주입을 두번해서 Http에서 에러가 발생한거 같습니다. 감사합니다~!

1 Like

안녕하세요.
저도 블레이저에서 인증 이슈를 겪었는데, 작성하신 코드가 제가 챗GPT를 통해 받은 코드와 매우 유사해서 제 경험을 남겨봅니다.
참고로 저는 닷넷코어8, 블레이저 서버 모델로 개발했습니다.

var result = await Http.PostAsJsonAsync("/issue-cookie", new { userId = "otpverified-user" });
if (result.IsSuccessStatusCode)
{
    // 쿠키 발급됨 → 페이지 새로고침해서 인증된 사용자로 전환
   Navigation.NavigateTo("/counter", forceLoad: true);
}

주석의 를 보니 GPT 코드로 보이는데, 이 코드만 보고 말씀드립니다.

HttpClient를 통해서 /issue-cookie에 접근하면 /issue-cookie에서 발급한 쿠키는 result객체에 저장되고 자동으로 사용자의 브라우저로 전달되지 않습니다.
(블레이저 서버 모델은 HttpClient가 서버 내부에서 동작하므로 보안상 올바른 동작입니다. 다른 모델은 테스트하지 않았지만 보안상 같은 동작을 할 것으로 생각합니다.)

여기서 문제는 인증 쿠키를 사용자 브라우저로 전달하지 않으므로 사용자 브라우저에는 인증 쿠키가 존재하지 않고 블레이저 페이지에 @attribute [Authorize] 특성을 부여하면 로그인 페이지로 돌아가게 됩니다. 물론 AuthorizeView 태그도 항상 NotAuthorized를 보여줍니다.

저도 아무런 의심없이 GPT가 작성해준 코드를 붙여넣고 동작하지 않길래 GPT를 갈구면서 Program.cs 만 열심히 고치다가, GPT 의존을 포기하고 직접 디버깅 하다보니 HttpClient는 브라우저와 독립적으로 서버에서 동작한다는 것이 뒤늦게 떠올랐습니다. (실제로는 서버에서 실행되지만 마치 브라우저에서 실행되는 것처럼 느껴지는 블레이저의 기술적 특징으로 HttpClient를 fetch로 착각하고 의심하지 않았던 겁니다.)
애초 브라우저로 쿠키가 전달되지 않는데 Program.cs를 암만 고쳐봐야 의미가 없는 행위였죠.

브라우저에 쿠키를 남기려면 HttpClient 결과에서 쿠키를 추출해서 JS interop을 통해 브라우저로 전달하는 코드를 추가하는 방법이 있지만, 저는 NavigationManager.NavigateTo("/issue-cookie", forceLoad: true)로 브라우저를 리다이렉트하도록 구현하는 방법을 추천합니다.
화면이 한 번 깜빡이는 단점이 있지만 거의 표준으로 사용되는 방법이라 브라우저의 보안 기능이 변경되어도 계속 동작할 가능성이 높습니다. (물론 블레이저 초기 로드 시간이 너무 오래 걸리면 HttpClient에서 쿠키를 추출해서 브라우저로 전달하는 방법을 이용하는게 좋습니다.)
리다이렉트 방법을 사용한다면 /issue-cookie도 Ok가 아니라 Redirect를 리턴하도록 변경해야 합니다.

2 Likes

오 맞습니다. GPT가 코드가 만들어주고 계속 쿠키가 발급이 안되어 GPT는 계속 도도리 답변만 내놓고 HttpClient가 서버에서 서버로 인증이라 브라우저에서 인증을 받아야 한다고 마지막에 알려주더라고요 ;; 코드만 보시고 GPT가 만들어준걸 알아채시다니 대단하십니다~!!