- 記事一覧 >
- ブログ記事
OpenResty(Nginx)LuaでCORS用に条件付きでレスポンスヘッダーを書き換え
はじめに
OpenResty(Nginx), Lua が使えるようにしたサーバーで、条件付きで CORS レスポンスヘッダーを書き換えることが必要になりました。
OpenResty(Nginx), Lua の環境については、別記事「OpenResty lua-resty-openidc php で SSO Web アプリ環境作成」を参照してください。
また、今回、SSO(シングルサインオン)有効で、GitLab が同居している Web アプリ環境で検証しました。「OpenResty lua-resty-openidc php で SSO Web アプリ環境作成」→「GitLab バンドル nginx 廃止 →OpenResty - GitLab - Web アプリ同居手順」の順に行ったら、その環境になる感じです。
CORS レスポンスヘッダーを書き換える理由は、以下のようにブラウザから直接 GitLab API へリクエストする場合、エラーになるからです。(private-token ヘッダーが許されず、CORS エラー)
また、Web アプリが認証有り、かつ、相手が GitLab API の場合、GitLab API 自身が、Access-Control-Allow-Origin: *
を返しているため、nginx のadd_header
では、対応できませんでした。
認証が有る場合、Access-Control-Allow-Origin: https://webapp
のように具体的に URL を指定しないといけなく、
nginx のadd_header
を使うと、ヘッダーの置き換えではなく、常に追加の動作のため、レスポンスヘッダーがAccess-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://webapp...(略)
と二重になり、
動作しませんでした。
● 認証情報有り & Lua(or more_set_headers)利用の場合
ヘッダー書き換え方法
いきなり、書き換え方法を書きますと、以下になります。(Lua でのヘッダー書き換え方法だけ知りたいという方は、このセクションで終わりです。)
location /users/logout {
proxy_cache off;
proxy_pass http://gitlab-workhorse;
header_filter_by_lua_block {
ngx.header['Set-Cookie'] = '_gitlab_session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
ngx.header['Content-Type'] = 'text/plain charset=UTF-8'
ngx.header['Content-Length'] = '0'
local origin = ngx.var.http_origin
if not origin then return end
ngx.header['Access-Control-Allow-Origin'] = origin
ngx.header['Access-Control-Allow-Credentials'] = 'true'
}
return 200;
}
location ~ /api/v\d/ {
proxy_cache off;
proxy_pass http://gitlab-workhorse;
header_filter_by_lua_block {
local origin = ngx.var.http_origin
if not origin then return end
ngx.header['Access-Control-Allow-Origin'] = origin
ngx.header['Access-Control-Allow-Credentials'] = 'true'
if ngx.req.get_method() == 'OPTIONS' then
ngx.header['Access-Control-Allow-Credentials'] = 'true'
ngx.header['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, private-token'
ngx.header['Access-Control-Allow-Methods'] = 'GET, DELETE, OPTIONS, POST, PUT'
ngx.header['Access-Control-Allow-Origin'] = origin
ngx.header['Access-Control-Max-Age'] = '2592000'
ngx.header['Content-Length'] = '0'
ngx.header['Content-Type'] = 'text/plain charset=UTF-8'
ngx.status = 204
return ngx.exit(ngx.status)
end
}
}
proxy_cache off;
proxy_pass http://gitlab-workhorse;
は、nginx の設定です。http://gitlab-workhorse
は、別の行に upstream gitlab-workhorse {
が書かれていて、GitLab へ右から左に流すというような意味です。
header_filter_by_lua_block {<Luaプログラム>}
は、コンテンツを生成後、レスポンスヘッダーを生成するタイミングで、 <Lua プログラム>
を実行してくださいという意味です。header_filter_by_lua
の部分は、実行したいタイミングにより、 access_by_lua
、 content_by_lua
...といろいろあります。 _block
の部分は、Lua プログラムをインラインで書くときに付ける接尾語です。
シングルクォート'で囲む場合、 _block
は必要ありませんが、 _block
無しは、nginx 設定文字列とみなされるため、内容によっては、エスケープしないといけなく、_block
無しは推奨されていないようです。_block
無し例:header_filter_by_lua 'ngx.header.Foo = "blah"';
local origin = ngx.var.http_origin
は、nginx の変数 $http_origin
を取り出しています。 $http_origin
は、リクエストヘッダーの Origin:
の値が入っています。nginx の変数は、 $request_method
や $request_uri
など、他に多数あります。
if not origin then return end
は、リクエストヘッダーの Origin:
が無かった場合、 return
で以降の処理を行わないという意味になります。
ngx.status = 204
で HTTP ステータスコード 204 を指定して、 ngx.exit(ngx.status)
で終了しています。 return 200;
の方は、nginx の設定になります。
header_filter_by_lua*
、balancer_by_lua*
、ssl_session_store_by_lua*
の中でreturn
無しで、ngx.exit(ngx.status)
とすると、ngx.exit
が非同期で動作して、下に書かれたコードが実行されます。常にreturn
と組み合わせて使うことが推奨されます。
各用語まとめ
【 CORS 】
クロスオリジンリソース共有 (Cross-Origin Resource Sharing) のことです。HTML,jsを得たサーバーと異なるオリジンからAjaxなどでデータを取り出す仕組みのことです。CORSが許可されていない場合、ブラウザがエラー判定します。
【 オリジン 】
URLの「スキーム」「ホスト」「ポート」の3つの組み合わせのことです。「スキーム」「ホスト」「ポート」のいずれかが異なる場合、たとえば、
https://example.com
とhttp://example.com
は別のオリジンです。http://example.com
とhttp://example.com:80
は表記方法が違うだけで、同一オリジンです。
【 Access-Control-Allow-Origin レスポンスヘッダー 】
指定されたオリジンからのリクエストを行うコードでレスポンスが共有できるかどうかを示します。
*または、CORSを許可するオリジン(URL)を指定します。
Cookieの送受信も許可したい場合は、ワイルドカード(*)の指定は許可されません。
【 Access-Control-Allow-Credentials レスポンスヘッダー 】
異なるオリジン間で、Cookie、認証ヘッダー、または TLS クライアント証明書のやり取りをする場合、
true
にセットします。XMLHttpRequest の
withCredential
をtrue
に設定( Ajaxの場合、xhrFields: { withCredentials: true }
)、fetchの場合、fetch(url, { credentials: 'include' })
)の場合に必要になります。
【 Access-Control-Allow-Methods レスポンスヘッダー 】
プリフライトリクエストのレスポンスの中で、リソースにアクセスするときに利用できる1つまたは複数のメソッドを指定します。
*または、メソッド(POST, GET, OPTIONS...)をカンマ区切りで指定します。
【 プリフライトリクエスト 】
サーバーがCORSに対応しているか確認するOPTIONSメソッドのリクエストです。
Content-Type に application/json や application/xml を使用、独自のヘッダーを設定などの条件のときにブラウザが確認が必要と判断して自動的に送信されます。
プリフライトリクエストの後、本来のリクエストがCORSエラーに該当する場合、本来のリクエストは送信されません。
【 Access-Control-Allow-Headers レスポンスヘッダー 】
プリフライトリクエストにAccess-Control-Request-Headersヘッダーが含まれていた場合、対応しているリクエストヘッダーを返します。
・リクエストヘッダー例:
Access-Control-Request-Headers: Content-Type, Accept
・レスポンスヘッダー例:
Access-Control-Allow-Headers: Content-Type, Accept
【 Access-Control-Max-Age レスポンスヘッダー 】
プリフライトリクエストの結果 (つまり Access-Control-Allow-Methods および Access-Control-Allow-Headers ヘッダーに含まれる情報) をキャッシュすることができる時間の長さを示します。
キャッシュするのは、ブラウザなので、サーバーからブラウザに「この期間だけ覚えておいてください」と指示するニュアンスになります。ブラウザによって、最長時間が決まっています。
動作検証
プリフライトリクエスト(正常)
プリフライトリクエスト(OPTIONS リクエスト)を送った場合、どういうレスポンスヘッダーが得られるか Chrome の DevTools で見てみました。
ヘッダーにprivate-tokenがあるため、プリフライトリクエストが発生します。
・検証プログラム
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CORS TEST</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function () {
$('#button').click(function () {
$.ajax({
url: 'https://gitlab-test.itccorporation.jp/api/v4/projects/?order_by=name&simple=true&sort=asc&per_page=100&page=1&_=1641522610939',
crossDomain: true,
headers: {
'private-token': 'DwDSWSBUGutGek_uFKpq',
},
method: 'GET',
dataType: 'json',
cache: false,
//xhrFields: {//認証情報を付加
// withCredentials: true
//},
})
.done(function (data, status, xhr) {
var data2 = JSON.stringify(data, null, 2);
console.log('succes: ' + data2);
$('#text').html(data2);
})
.fail(function (xhr, status, error) {
alert('error');
});
});
});
</script>
</head>
<body>
<input type="button" id="button" value="button" />
<br />
<pre id="text"></pre>
</body>
</html>
・リクエストヘッダー
accept: */*
accept-encoding: gzip, deflate, br
accept-language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7
access-control-request-headers: private-token
access-control-request-method: GET
cache-control: no-cache
origin: https://gitlab-download-app.itccorporation.jp
pragma: no-cache
referer: https://gitlab-download-app.itccorporation.jp/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
・レスポンスコード
204
・レスポンスヘッダー
access-control-allow-credentials: true
access-control-allow-headers: Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since,private-token
access-control-allow-methods: GET, DELETE, OPTIONS, POST, PUT
access-control-allow-origin: https://gitlab-download-app.itccorporation.jp
access-control-max-age: 2592000
content-length: 0
date: Thu, 06 Jan 2022 13:34:27 GMT
server: openresty
プリフライトリクエスト(CORS エラー)
ngx.header['Access-Control-Allow-Headers'] =
からprivate-token
を削除した場合、CORS エラーになります。その場合のプリフライトリクエスト結果は、以下です。
通信結果は、特に変わりません。エラーになるのは、本来のリクエストですので、普通にやり取りをしている動きになります。
・リクエストヘッダー
accept: */*
accept-encoding: gzip, deflate, br
accept-language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7
access-control-request-headers: private-token
access-control-request-method: GET
cache-control: no-cache
origin: https://gitlab-download-app.itccorporation.jp
pragma: no-cache
referer: https://gitlab-download-app.itccorporation.jp/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
・レスポンスコード
204
・レスポンスヘッダー
access-control-allow-credentials: true
access-control-allow-headers: Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since
access-control-allow-methods: GET, DELETE, OPTIONS, POST, PUT
access-control-allow-origin: https://gitlab-download-app.itccorporation.jp
access-control-max-age: 2592000
content-length: 0
date: Thu, 06 Jan 2022 13:51:27 GMT
server: openresty
本来のリクエスト
プリフライトリクエストの結果、CORS OK 判定の場合、本来のリクエストが送信されます。その結果は以下です。
プリフライトリクエストがある場合、CORS エラーは、リクエストを送信する前にエラーになります。
プリフライトリクエストが無い場合、CORS エラーは、リクエストを送信した後にエラーになります。
CORS エラーは、
"xhrFields: {withCredentials: true}
で認証情報を送信しているにもかかわらず、レスポンスヘッダーがAccess-Control-Allow-Origin: *
で具体的なオリジンを指定していない。"
等の場合に発生します。
・リクエストヘッダー
accept: application/json, text/javascript, */*; q=0.01
accept-encoding: gzip, deflate, br
accept-language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7
cache-control: no-cache
origin: https://gitlab-download-app.itccorporation.jp
pragma: no-cache
private-token: DwDSWSBUGutGek_uFKpq
referer: https://gitlab-download-app.itccorporation.jp/
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
・レスポンスコード
200
・レスポンスヘッダー
access-control-allow-credentials: true
access-control-allow-methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-origin: https://gitlab-download-app.itccorporation.jp
access-control-expose-headers: Link, X-Total, X-Total-Pages, X-Per-Page, X-Page, X-Next-Page, X-Prev-Page, X-Gitlab-Blob-Id, X-Gitlab-Commit-Id, X-Gitlab-Content-Sha256, X-Gitlab-Encoding, X-Gitlab-File-Name, X-Gitlab-File-Path, X-Gitlab-Last-Commit-Id, X-Gitlab-Ref, X-Gitlab-Size
access-control-max-age: 7200
cache-control: max-age=0, private, must-revalidate
content-type: application/json
date: Thu, 06 Jan 2022 14:18:27 GMT
etag: W/"1e9f9e9297c9bcdd2decdb97e87ba9b0"
link: <http://gitlab-test.itccorporation.jp:443/api/v4/projects?_=1641532610550&membership=false&order_by=name&owned=false&page=1&per_page=100&simple=true&sort=asc&starred=false&statistics=false&with_custom_attributes=false&with_issues_enabled=false&with_merge_requests_enabled=false>; rel="first", <http://gitlab-test.itccorporation.jp:443/api/v4/projects?_=1641532610550&membership=false&order_by=name&owned=false&page=1&per_page=100&simple=true&sort=asc&starred=false&statistics=false&with_custom_attributes=false&with_issues_enabled=false&with_merge_requests_enabled=false>; rel="last"
server: openresty
vary: Origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-gitlab-feature-category: projects
x-next-page
x-page: 1
x-per-page: 100
x-prev-page
x-request-id: 01FRSFJME1FS9N4S5TJ5MT7R36
x-runtime: 0.061306
x-total: 4
x-total-pages: 1
access-control-expose-headers
GitLab API からは、access-control-expose-headers レスポンスヘッダーが返ってきます。これは、何かと言うと、クライアント側でこのレスポンスヘッダーを使って良いというものです。
通信成功時処理を以下のようにして、確認してみました。
.done(function (data, status, xhr) {
console.log(xhr.getAllResponseHeaders());
$("#text").html(xhr.getAllResponseHeaders());
})
レスポンス:
cache-control: max-age=0, private, must-revalidate
content-type: application/json
link: ; rel="first", ; rel="last"
x-next-page:
x-page: 1
x-per-page: 100
x-prev-page:
x-total: 4
x-total-pages: 1
x-content-type-options
、x-frame-options
、x-gitlab-feature-category
がxhr.getAllResponseHeaders()
で取得できません。すなわち、access-control-expose-headers
ヘッダー記載のヘッダーだけプログラムで参照できます。
more_set_headers について
ヘッダー書き換えは、nginx 拡張モジュールmore_set_headers
を使う方法もあります。nginx 標準のadd_header
の場合、常に追加になりますが、more_set_headers
の場合は、無ければ追加、有れば置き換えの動作になります。
したがって、以下のような設定で、今回 Lua でやったことと同じ動作をさせることができます。
more_set_headersの導入方法の説明は、割愛します。
location /users/logout {
proxy_cache off;
proxy_pass http://gitlab-workhorse;
if ( $http_origin != "" ) {
more_set_headers "Access-Control-Allow-Origin: $http_origin";
more_set_headers "Access-Control-Allow-Credentials: true";
}
more_set_headers "Set-Cookie:_gitlab_session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;";
more_set_headers "Content-Type: text/plain charset=UTF-8";
more_set_headers "Content-Length: 0";
return 200;
}
location ~ /api/v\d/ {
proxy_cache off;
proxy_pass http://gitlab-workhorse;
more_set_headers "Access-Control-Allow-Origin: $http_origin";
more_set_headers "Access-Control-Allow-Credentials: true";
if ($request_method = "OPTIONS") {
more_set_headers "Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, private-token";
more_set_headers "Access-Control-Allow-Methods: GET, DELETE, OPTIONS, POST, PUT";
more_set_headers "Access-Control-Max-Age: 2592000";
more_set_headers "Content-Length: 0";
more_set_headers "Content-Type: text/plain charset=UTF-8";
return 204;
}
}
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。