AJAXでファイルをダウンロードする
環境
HTML5に対応したブラウザ
やり方
汎用的にfunctionを作ってみた。
function downloadFile(ajaxUrl, ajaxData, fileName) { $.ajax({ type: "post", url: ajaxUrl, data: ajaxData, xhrFields: { responseType: 'blob' } }).done(function (response, _textStatus, _jqXHR) { if (window.navigator.msSaveBlob) { // IE window.navigator.msSaveBlob(response, fileName); } else { // IE以外 $('<a>', { href: URL.createObjectURL(new Blob([response], { type: response.type })), download: fileName }).appendTo(document.body)[0].click(); } }); } // 使い方 downloadFile("/download", $('form').serialize(), "download.zip");
説明
xhrFields: { responseType: 'blob' }
は$.ajax
でバイナリデータを受け取るために必要なパラメーター。
参考
ASP.NET + Sustainsys.Saml2でシングルサインオンを実装する
環境
- ASP.NET MVC 5
- Sustainsys.Saml2 2.3
Sustainsys.Saml2とは
ASP.NET用のSAML2認証ライブラリー saml2.sustainsys.com
サンプルコード
githubからサンプルコードをダウンロードできるので、それをベースに作るのがおすすめ。 github.com SamplesフォルダーにMVC用、Web Forms用、Owin用などが入っている。ライブラリーがプロジェクト参照になっているので、開発する前にNuGetパッケージを参照するように変更したほうがいいと思う。
開発用スタブIdP
公式がWEBで公開している他、前述のサンプルコードにも入っている。
実装
以下はMVC用のサンプルをベースにした例。
やりたいこと
- IdPは固定で1つだけ。Discovery Serviceは使わない。
- リクエスト・・・HTTP Redirect Binding
- レスポンス・・・HTTP POST Binding
- リクエスト・レスポンス共に署名する。
- 認証後はユーザー情報をセッションに格納しておく。
設定
Web.configのsustainsys.saml2要素を以下のように変更。
<sustainsys.saml2 entityId="http://localhost:2181/Saml2" returnUrl="http://localhost:2181/Home/Index" authenticateRequestSigningBehavior="Always"> <identityProviders> <add entityId="https://stubidp.sustainsys.com/Metadata" signOnUrl="https://stubidp.sustainsys.com/" allowUnsolicitedAuthnResponse="true" binding="HttpRedirect" wantAuthnRequestsSigned="true"> <signingCertificate fileName="~/App_Data/stubidp.sustainsys.com.cer"/> </add> </identityProviders> <serviceCertificates> <add fileName="~/App_Data/Sustainsys.Saml2.Tests.pfx"/> </serviceCertificates> </sustainsys.saml2>
設定はIdP次第なのであくまで一例として。サンプルとの相違点は以下のとおり。
- Discovery Serviceを使わないのでdiscoveryServiceUrl属性とfederations要素を削除
- リクエストに署名するためauthenticateRequestSigningBehavior="Always"追加
- identityProviderの属性にwantAuthnRequestsSigned="true"追加
ちなみに署名をファイルで指定しているが、本番リリースの際は証明書ストアを使うべき、とのこと。
認証後処理の追加
Global.asax.csを以下のように変更。
using SampleMvcApplication.Models; using Sustainsys.Saml2.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Web; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace SampleMvcApplication { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); Saml2Controller.Options.Notifications.AcsCommandResultCreated = (cr, r) => { // クレームからIDを取得 string userId = cr.Principal.Claims.Single(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value; // なんらかの方法でユーザー情報を取得 UserModel userInfo = GetUserInfo(userId); // セッションにユーザー情報を格納 HttpContext.Current.Session["USER_INFO"] = userInfo; }; } } }
Saml2Controller.Options.Notifications.[Notification名]
にコールバック関数を設定することで、ライブラリーの中の様々なポイントに処理を追加したりカスタマイズしたりできる。AcsCommandResultCreated
は一連の認証プロセスの最後に発生するので、ユーザー情報の取得とセッションへの格納はここに追加する。
認証時に発生するNotificationの例をいくつか挙げると、
Notification名 | 発生タイミング・用途 |
---|---|
AuthenticationRequestCreated | 認証リクエスト作成後に発生。リクエストをカスタマイズしたい場合はここで。 |
SignInCommandResultCreated | IdPへのリダイレクトを返す前に発生 |
AcsCommandResultCreated | ACSの処理が終わった後に発生 |
ValidateAbsoluteReturnUrl | ReturnUrlに絶対パスを指定されたとき発生。パスのOK・NGを判定する関数を設定する。デフォルトだと絶対パスは常にNG。 |
SelectIdentityProvider | 認証に使用するIdPを決定する際に発生。IdPオブジェクトを返す関数を設定すると既定のロジックの代わりにこちらで決定される。 |
などがある。他にも色々あるので詳細はSaml2Notificationsクラスのソースを参照。
Saml2/Saml2Notifications.cs at master · Sustainsys/Saml2 · GitHub
あとがき
充実したサンプルにスタブIdPとまさに至れり尽くせりなライブラリー。まあ欲を言えば、Notificationの使い方はドキュメントに書いておいてほしかったけど。
C#で共有フォルダーを作成する
やり方
net shareコマンドを使う方法とWin32 Apiを使う方法がある。以下、MyNewShare
という名前でC:\Share
を共有し、EveryoneにFull Controlを付与するサンプル。
なお、どちらの方法でも管理者権限が必要となる。
net shareコマンドを使う方法
ProcessStartInfo info = new ProcessStartInfo("net", @"share MyNewShare=C:\Share /grant:everyone,full"); info.CreateNoWindow = true; Process.Start(info);
Win32 Apiを使う方法
// フォルダーの共有 ManagementClass share = new ManagementClass("Win32_Share"); ManagementBaseObject inParams = share.GetMethodParameters("Create"); inParams["Name"] = "MyNewShare"; inParams["Path"] = @"C:\Share"; inParams["Type"] = 0x0; // Disk Drive ManagementBaseObject outParams = share.InvokeMethod("Create", inParams, null); if ((uint)(outParams.Properties["ReturnValue"].Value) != 0) { throw new Exception("フォルダーの共有に失敗しました。"); } // 権限の設定 // Everyoneを選択 NTAccount ntAccount = new NTAccount("Everyone"); // SID取得 SecurityIdentifier userSID = (SecurityIdentifier)ntAccount.Translate(typeof(SecurityIdentifier)); byte[] utenteSIDArray = new byte[userSID.BinaryLength]; userSID.GetBinaryForm(utenteSIDArray, 0); // Trustee ManagementClass userTrustee = new ManagementClass("Win32_Trustee"); userTrustee["Name"] = "Everyone"; userTrustee["SID"] = utenteSIDArray; // ACE ManagementClass userACE = new ManagementClass("Win32_Ace"); userACE["AccessMask"] = 2032127; // Full access userACE["AceFlags"] = AceFlags.ObjectInherit | AceFlags.ContainerInherit; userACE["AceType"] = AceType.AccessAllowed; userACE["Trustee"] = userTrustee; // SecurityDescriptor ManagementClass userSecurityDescriptor = new ManagementClass("Win32_SecurityDescriptor"); userSecurityDescriptor["ControlFlags"] = 4; // SE_DACL_PRESENT userSecurityDescriptor["DACL"] = new object[] { userACE }; // 共有フォルダーに権限をセット ManagementObject myNewShare = new ManagementObject(share.Path + ".Name='MyNewShare'"); object result = myNewShare.InvokeMethod("SetShareInfo", new object[] { Int32.MaxValue, null, userSecurityDescriptor }); if ((uint)result != 0) { throw new Exception("権限の設定に失敗しました。"); }
解説
net shareコマンドを使おう。
と言いたいところだが、もっと汎用的な作りにしたいのなら、引数を文字列で組み立てなければならないnet shareよりWin32 Apiのほうがいいかもしれない。また、権限の設定を省略するとEveryoneにRead権限のみが付与されるのだが、それでよければWin32 Apiでもかなりコードを短縮*1できる。
参考
*1:"権限の設定"というコメント以降を省略できる
Datatables小ネタ集
Datatablesとは
DataTables | Table plug-in for jQuery
行選択イベント
行選択時のイベントを捕まえる方法は二通りある。
select/deselectイベント
- ユーザーによる選択でもコードによる選択でも発火する。
- タイミングは選択された「後」。
- singleモードだとselectより先に前に選択されていた行のdeselectが発火する。
user-selectイベント
- ユーザーによる選択のみで発火する。
- タイミングは選択される「前」、なのでキャンセルが可能。
- selectでもdeselectでも発火する。クリックされたのが選択行か、未選択行かで判別できる。
AJAXリクエストを中止する
table.settings()[0].jqXHR.abort();
リクエスト開始前だとtable.settings()[0].jqXHR
がnullなので注意。
FixedColumnsとinput
FixedColumnsで作成した固定列にinputを配置するとpostのときに同じ名前の値が2つ飛ぶ。これは固定列が実はクローンであり、裏側にオリジナルが隠れているため。クライアント側、またはサーバー側でなんらかの対応が必要。
ちなみに縦スクロールを有効にした場合のヘッダー・フッターもクローンなのでinputを配置すると同じ問題が発生する。というわけでinputを固定列やヘッダー・フッターに配置するのはなるべく避けたい。
セルの結合
tbodyでのrowspanはできない。
inputを配置した列のソート
通常、ソートには内部のキャッシュが利用されるためロード後に入力された値はソートに反映されない。入力された値でソートしたい場合は下記のリンクを参照。
DataTables example - Live DOM ordering
inputを配置した列のファイル出力
こちらも同様、キャッシュを利用しているためロード後に入力された値は出力に反映されない。以下は列のrenderオプションとbuttonsのexportOptionsを利用して入力された値を出力するサンプル。
$('#table1').DataTable({ dom: 'Bfrtip', columns: [ { // inputの値 render: function (data, type, row, meta) { if (type === 'export') { var input = $('<div>' + data + '</div>').find('input'); data = $('[name="' + input.attr("name") + '"]').val(); } return data; } }, { // selectのテキスト render: function (data, type, row, meta) { if (type === 'export') { var input = $('<div>' + data + '</div>').find('select'); data = $('[name="' + input.attr("name") + '"] option:selected').text(); } return data; } }, { // チェックボックス・ラジオボタン render: function (data, type, row, meta) { if (type === 'export') { var input = $('<div>' + data + '</div>').find('input'); data = $('[name="' + input.attr("name") + '"][value="' + input.attr("value") + '"]').prop('checked').toString(); } return data; } } ], buttons: [{ bom: true, // エクセルで文字化けしないようにbom付き extend: 'csv', exportOptions: { orthogonal: 'export' } }] });
renderの引数、data
にはセルデータが入っているけど、これは前述の通り、ロード時にキャッシュされた古いデータ。なので取り出したinputのname属性をキーにして最新のinputを取得しなおす。
AJAX処理を順番に実行する
やりたいこと
複数のAJAX処理を一つずつ順番に、前の処理が終わるのを待って実行したい。
環境
jQuery 1.8 以降
やり方
jQueryのDeferredオブジェクトを利用してAJAX実行Queueを作成する。途中でエラーが発生した際にそのまま続行するQueueと以降の処理をキャンセルするQueue、2通り作ってみた。
エラー後も続行するQueue
// AJAX実行Queue var ajaxQueue = function () { var previous = new $.Deferred().resolve(); return function (fn) { // then()の第1と第2引数に同じ関数を渡す return previous = previous.then(fn, fn); }; }(); // キューが空なので即実行される ajaxQueue(function () { return $.get('/first'); }); // 1つ目の処理が終わったら実行される ajaxQueue(function() { return $.get('/second'); });
エラー後はキャンセルするQueue
この場合、一度エラーが発生すると以降常にキャンセルされ続けてしまうため、再び処理を開始するための初期化関数も用意した。
// AJAX実行Queue var ajaxQueue = function () { var previous = new $.Deferred().resolve(); return { put: function (fn) { // then()の第1引数のみ渡す return previous = previous.then(fn); }, init: function () { // Deferredオブジェクトの初期化 previous = new $.Deferred().resolve(); } }; }(); // キューが空なので即実行される ajaxQueue.put(function () { return $.get('/first'); }); // 1つ目の処理でエラーが発生していなければ実行される ajaxQueue.put(function() { return $.get('/second'); }); // 初期化 ajaxQueue.init();
補足
AJAX処理に.done()
や.fail()
が登録されている場合、それらの実行まで完了してから次の処理が開始される。
おまけ
前の処理が終わっていないときは次の処理を阻止したい、という場合。
var ajaxQueue = function () { var previous = new $.Deferred().resolve(); return function (fn) { if (previous.state() == 'pending') { alert('通信中です。しばらくお待ち下さい。'); return; } return previous = previous.then(fn, fn); }; }();
参考
ファイルダウンロード用のViewを自作する
Springでファイルダウンロードを実装する方法はいろいろあるが、今回はViewを自作する方向でやってみたい。
利点
- レスポンスヘッダーの設定など、冗長になりがちなコードをコントローラーに書かなくて済む。
- 戻り値の型を文字列で統一できるので、条件によってダウンロードか画面表示かに分岐するようなコントローラーが作れる。
環境
Spring Boot 1.4
やり方
ファイルの種類ごとにViewを作成する。以下はPDFの例。
View
ファイルダウンロードの体裁を整えるクラス。コントローラーとのデータの受け渡しはModelを通して行う。ここではファイルデータをcontents
、ファイル名をfileName
という名前で受け取ることにしている。
import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.util.FileCopyUtils; import org.springframework.web.servlet.view.AbstractView; public class PdfView extends AbstractView { @Override protected boolean generatesDownloadContent() { return true; } @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { byte[] pdf = (byte[]) model.get("contents"); String fileName = (String) model.get("fileName"); response.setHeader("Content-Disposition", "attachment; filename*=utf-8''" + java.net.URLEncoder.encode(fileName, "UTF-8")); response.setContentLength(pdf.length); response.setContentType("application/pdf"); FileCopyUtils.copy(pdf, response.getOutputStream()); } }
設定
自作したViewとBeanNameViewResolver
をBean定義する。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.BeanNameViewResolver; @Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { @Bean public BeanNameViewResolver fileDownloadViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(0); return resolver; } @Bean public PdfView pdfView() { return new PdfView(); } }
Controller
先に書いた通り、ファイルデータをcontents
、ファイル名をfileName
という名前でModelにセットした後、自作したViewのBean名、pdfView
を戻り値として返す。
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class RootController { @GetMapping("pdf") public String pdf(Model model) { // PDFファイルの取得 byte[] contents = getPdf(); model.addAttribute("contents", contents); model.addAttribute("fileName", "テスト.pdf"); return "pdfView"; } }
動的に追加される要素にイベントハンドラーをアタッチしたい
例えばテーブルの各行にクリックイベントをアタッチしたいとき、それがAJAXで非同期にデータをロードするテーブルならばロードのたびにアタッチし直さなければならないし、ユーザーが自由に行追加できるテーブルならばその都度追加行にアタッチしなければならない。
動的にイベントをアタッチするようなコードは、同じイベントを二重にアタッチしてしまう、といった事故を起こしがち。できればイベントのアタッチはページロード時に一発で済ませたい。そんなときはdelegatedなイベントハンドラーを利用するとよい。
環境
jQuery 1.7以降
delegatedなイベントハンドラーとは
イベントの発生元に直接ハンドラーをアタッチするのではなく、親要素にアタッチして子から伝播してきたところを捕まえよう、というコンセプトのハンドラー。
やり方
冒頭に書いたテーブルの例だと以下の通り。
$( "#dataTable tbody" ).on( "click", "tr", function() { console.log( $( this ).text() ); });
説明
tr
ではなくその親要素、tbody
にイベントをアタッチする。イベントの発生元が第2引数のセレクターtr
にマッチしない限りイベントは発火しない。イベントを捕まえるのはtbody
なので、子要素のtr
が増えたり書き換わったりしても影響を受けない。
注意
Attaching many delegated event handlers near the top of the document tree can degrade performance.(中略)For best performance, attach delegated events at a document location as close as possible to the target elements. Avoid excessive use of
document
ordocument.body
for delegated events on large documents.
.on() | jQuery API Documentation
ドキュメントツリーのトップに近い要素にdelegatedなイベントハンドラーを多数アタッチするとパフォーマンスが落ちる可能性がある。document
とかdocument.body
とかにやたらとアタッチするのは避け、できるだけターゲットに近い要素にアタッチしましょう、とのこと。