javascript - Download Seguro de Arquivo Usando Blazor Webassembly e ASP.NET Core
Consegui encontrar uma solução aqui que mostra como criar um controlador e baixar arquivos usando injeção de JS: Como alguém pode gerar e salvar um arquivo no lado do cliente usando Blazor?
No entanto, adicionar o atributo [Authorize] ao controlador bloqueia qualquer tentativa (mesmo que esteja logado) de baixar o arquivo. Eu quero que apenas pessoas autorizadas tenham acesso para baixar os arquivos.
O resto do site está usando JWT sem problemas.
Minha pergunta é: como adiciono autenticação JWT a esse recurso de download de arquivos? Ou existe uma maneira alternativa? Os arquivos estão no sistema de arquivos do servidor e a abordagem acima é muito gentil com a memória, então prefiro me manter longe de blobs.
Observação: Estou usando contas de usuário no aplicativo.
Resposta #1
Para proteger o download de um arquivo, eu uso um token de uso único enviado na URI de solicitação de download:
- Defina uma classe para armazenar um token único
public class OneTimeToken
{
public string Id { get; set; }
public string ClientId { get; set; }
public string UserId { get; set; }
public string Data { get; set; }
}
Prefiro armazenar tokens no banco de dados, mas você pode escolher armazená-los na memória, mas obviamente no lado do servidor.
- Antes de baixar, crie um token
Aqui eu uso um serviço chamando uma API para criar meu token
public class OneTimeTokenService
{
private readonly IAdminStore<OneTimeToken> _store; // this my service calling the API
private readonly AuthenticationStateProvider _stateProvider;
private readonly IAccessTokenProvider _provider;
private readonly IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> _options;
public OneTimeTokenService(IAdminStore<OneTimeToken> store,
AuthenticationStateProvider state,
IAccessTokenProvider provider,
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_stateProvider = state ?? throw new ArgumentNullException(nameof(state));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public async Task<string> GetOneTimeToken()
{
// gets the user access token
var tokenResult = await _provider.RequestAccessToken().ConfigureAwait(false);
tokenResult.TryGetToken(out AccessToken token);
// gets the authentication state
var state = await _stateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
// creates a one time token
var oneTimeToken = await _store.CreateAsync(new OneTimeToken
{
ClientId = _options.Value.ProviderOptions.ClientId,
UserId = state.User.Claims.First(c => c.Type == "sub").Value,
Expiration = DateTime.UtcNow.AddMinutes(1),
Data = token.Value
}).ConfigureAwait(false);
return oneTimeToken.Id;
}
}
- Crie o URI de download quando o usuário clicar no link de download
Aqui eu uso um botão, mas funciona com qualquer elemento HTML, você pode usar um link no lugar.
@inject OneTimeTokenService _service
<button class="btn btn-secondary" @onclick="Download" >
<span class="oi oi-arrow-circle-top"></span><span class="sr-only">Download
</span>
</button>
@code {
private async Task Download()
{
var token = await _service.GetOneTimeToken().ConfigureAwait(false);
var url = $"http://locahost/stuff?otk={token}";
await _jsRuntime.InvokeVoidAsync("open", url, "_blank").ConfigureAwait(false);
}
}
- Recupere o token da URL
4.1. Adicione o pacote IdentityServer4.AccessTokenValidation ao seu projeto de API.
No método ConfigureServices da Startup, use a autenticação do IdentityServer:
services.AddTransient<OneTimeTokenService>()
.AddAuthentication()
.AddIdentityServerAuthentication(options =>
{
options.TokenRetriever = request =>
{
var oneTimeToken = TokenRetrieval.FromQueryString("otk")(request);
if (!string.IsNullOrEmpty(oneTimeToken))
{
return request.HttpContext
.RequestServices
.GetRequiredService<OneTimeTokenService>()
.GetOneTimeToken(oneTimeToken);
}
return TokenRetrieval.FromAuthorizationHeader()(request);
};
});
- Defina um serviço para ler e consumir o token de uso único a partir do URI
O token não deve ser reutilizável, então ele é excluído a cada solicitação.
Aqui é apenas um exemplo. Se você armazenar tokens no banco de dados, pode usar um contexto EF; se estiver na memória, pode usar um cache de objeto, por exemplo.
public class OneTimeTokenService{
private readonly IAdminStore<OneTimeToken> _store;
public OneTimeTokenService(IAdminStore<OneTimeToken> store)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
}
public string GetOneTimeToken(string id)
{
// gets the token.
var token = _store.GetAsync(id, new GetRequest()).GetAwaiter().GetResult();
if (token == null)
{
return null;
}
// deletes the token to not reuse it.
_store.DeleteAsync(id).GetAwaiter().GetResult();
return token.Data;
}
}
