我有一个Web应用程序,其中Angular(7)前端与服务器上的REST API通信,并使用OpenId Connect(OIDC)进行身份验证。我正在使用一个HttpInterceptor
头,它在我的HTTP请求中添加了一个Authorization
头,以向服务器提供auth令牌。到目前为止,一切都很好。
但是,除了传统的JSON数据外,我的后端还负责即时生成文档。在添加身份验证之前,我可以简单地链接到这些文档,如:
<a href="https://my-server.com/my-api/document?id=3">Download</a>
但是,现在我添加了身份验证,该操作不再起作用,因为在获取文档时浏览器未在请求中包括auth令牌-因此,我从服务器收到了401-Unathorized
响应。
因此,我不能再依赖原始的HTML链接-我需要创建自己的HTTP请求,并显式添加auth令牌。但是,如何确保用户体验与用户单击链接一样?理想情况下,我希望使用服务器建议的文件名而不是通用文件名来保存文件。
答案 0 :(得分:1)
我将部分基于this answer的“可以在我的机器上工作的东西”和其他类似的东西拼凑在一起-尽管我的努力是通过打包为可重复使用的指令来实现“角度化”。没什么可做的(大部分代码都在根据服务器发送的content-disposition
标头来确定文件名应该是什么)。
download-file.directive.ts :
import { Directive, HostListener, Input } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Directive({
selector: '[downloadFile]'
})
export class DownloadFileDirective {
constructor(private readonly httpClient: HttpClient) {}
private downloadUrl: string;
@Input('downloadFile')
public set url(url: string) {
this.downloadUrl = url;
};
@HostListener('click')
public async onClick(): Promise<void> {
// Download the document as a blob
const response = await this.httpClient.get(
this.downloadUrl,
{ responseType: 'blob', observe: 'response' }
).toPromise();
// Create a URL for the blob
const url = URL.createObjectURL(response.body);
// Create an anchor element to "point" to it
const anchor = document.createElement('a');
anchor.href = url;
// Get the suggested filename for the file from the response headers
anchor.download = this.getFilenameFromHeaders(response.headers) || 'file';
// Simulate a click on our anchor element
anchor.click();
// Discard the object data
URL.revokeObjectURL(url);
}
private getFilenameFromHeaders(headers: HttpHeaders) {
// The content-disposition header should include a suggested filename for the file
const contentDisposition = headers.get('Content-Disposition');
if (!contentDisposition) {
return null;
}
/* StackOverflow is full of RegEx-es for parsing the content-disposition header,
* but that's overkill for my purposes, since I have a known back-end with
* predictable behaviour. I can afford to assume that the content-disposition
* header looks like the example in the docs
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
*
* In other words, it'll be something like this:
* Content-Disposition: attachment; filename="filename.ext"
*
* I probably should allow for single and double quotes (or no quotes) around
* the filename. I don't need to worry about character-encoding since all of
* the filenames I generate on the server side should be vanilla ASCII.
*/
const leadIn = 'filename=';
const start = contentDisposition.search(leadIn);
if (start < 0) {
return null;
}
// Get the 'value' after the filename= part (which may be enclosed in quotes)
const value = contentDisposition.substring(start + leadIn.length).trim();
if (value.length === 0) {
return null;
}
// If it's not quoted, we can return the whole thing
const firstCharacter = value[0];
if (firstCharacter !== '\"' && firstCharacter !== '\'') {
return value;
}
// If it's quoted, it must have a matching end-quote
if (value.length < 2) {
return null;
}
// The end-quote must match the opening quote
const lastCharacter = value[value.length - 1];
if (lastCharacter !== firstCharacter) {
return null;
}
// Return the content of the quotes
return value.substring(1, value.length - 1);
}
}
它的用法如下:
<a downloadFile="https://my-server.com/my-api/document?id=3">Download</a>
...或者,当然:
<a [downloadFile]="myUrlProperty">Download</a>
请注意,我没有在此代码中向HTTP请求中明确添加auth令牌,因为我的HttpClient
已经对 all HttpInterceptor
调用进行了处理实现(未显示)。在没有拦截器的情况下执行此操作仅是在请求中添加标头(在我的情况下为Authorization
标头)。
还有一件值得一提的事情是,如果被调用的Web API在使用CORS的服务器上,则可能会阻止客户端代码访问内容处置响应标头。要允许访问此标头,可以让服务器发送适当的access-control-allow-headers
标头。
答案 1 :(得分:0)
Angular(7)前端与服务器上的REST API通信
然后:
<a href="https://my-server.com/my-api/document?id=3">Download</a>
告诉我您的RESTful API并不是真正的RESTful。
原因是上述GET请求不属于RESTful API范式的一部分。这是一个基本的HTTP GET请求,它产生一个非JSON内容类型的响应,并且该响应不代表RESTful资源的状态。
这仅仅是URL语义,并没有真正改变任何东西,但是当您开始将它们混合到混合API中时,您的确会遇到这类问题。
但是,现在我已经添加了身份验证,该方法不再起作用,因为在获取文档时浏览器未在请求中包括auth令牌。
否,它工作正常。产生401
未经授权的响应的是服务器。
我明白您的意思。 <a>
标签不再允许您下载URL,因为该URL现在需要身份验证。话虽如此,服务器在无法提供GET的情况下要求GET请求的HEADER身份验证有点奇怪。这不是您的经验所独有的问题,因为我已经看到这种情况经常发生。这是切换到JWT令牌并认为这可以解决所有问题的心态。
使用createObjectURL()
将响应更改为新窗口是一种具有其他副作用的技巧。例如弹出窗口阻止程序,浏览器历史记录突变以及用户无法查看下载,中止下载或在其浏览器的下载历史记录中查看它。您还必须知道下载在浏览器中消耗的所有内存,而切换到base64只会增加内存消耗。
您应该通过修复服务器的身份验证来解决此问题。
<a href="https://my-server.com/my-api/document?id=3&auth=XXXXXXXXXXXXXXXXXXXX">Download</a>
混合RESTful API应该使用混合身份验证方法。