見出し画像

いつものカードを使って認証処理を実装してみませんか?(No.060)

以前からNFCカード(非接触電子カード)を使って、ログイン処理やタイムカードのような業務を実現したいと思っていました。最近のスマホでもNFCリーダが使えるのですが、WebアプリとなるとJavascript言語とブラウザで使えることが条件となり、現時点ではAndroid系のみブラウザがサポートしていなく、時期を待っていました。
今回は、実装方式を見直して、パソコン(windows OS)で使えるカードリーダと組み合わせる方式で実装してみました。
カードリーダもバージョンアップされているようで、最新は、今回使った機種ではありませんが、どちらでも動作するようです。
今回は、SONY製のRC-S380を使いました。
構成は以下のようになります。
業務アプリ(Webアプリ)は、今までも紹介しています環境と同じです。
画面は、Javascript言語とWebixライブラリを使用します。
業務アプリ(サーバアプリ)は、PHP言語で実装します。

今回、ICカードリーダを使うので、Linuxサーバとは、別にWindowsOSを準備しています。サーバでも実装できると思いますが、通常のWindows10を使っています。そのWindows10にUSB接続でカードリーダを接続します。
WindowsOS上には、C#のアプリを実装してWebサーバからのリクエスト(カード情報読取り)を受信する機能と、そのリクエストに応じて、カードリーダを使ってNFCカードの情報を読み出す機能を実装します。
SONY製なので、SONYが公開しているドライバやアプリもインストールしていますが、C#からのアクセスは、PCSC Sharpというライブラリをインストールして使いました。
参考にした記事は、以下の記事です。

Webサーバからのリクエストに応じてカードリーダから読み込み処理をするのですが、リクエストと、実際にカードをリーダにあてるタイミングに時間差があることも考慮し、リクエストを受信後に10秒回の間に500ms周期でカードリーダを読取り、2回連続して同じデータが読めたらその情報を返し、読めない場合は、ブランク情報を返すようにC#のアプリで実装しました。

参考にした記事では、リーダからの読み出しイベントを使っていますが、今回は、周期的に読む実装にしました。

また、Linuxサーバとカードリーダを実装したWindowsOS間の通信は、httpによるリクエストで応答する実装にしました。
WindowsOS上に簡易Webサーバを実装します。ポート番号は8082です。
PHP言語からcurlコマンドでリクエストを出して、簡易Webサーバで受信する動作となります。
(C#のアプリは、管理者モードで実行する必要があります)

まずは、C#のアプリを紹介します。
画面は、起動以外は、検証用にボタンを配置していますが、リクエストは、httpで受信するので、使用しません。
動作確認用にログを出力したいので、NlogコンポーネントもNuGetでインストールします。カードリーダをつかうので、NuGetでPCSC関連のコンポーネントもインストールします。
また、簡易Webサーバを構築するために、いくつかコンポーネントをインストールしています。


画面でデザインです。

C# Form1.csのソースです。

using PCSC;
using PCSC.Exceptions;
using PCSC.Iso7816;
using PCSC.Monitoring;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Configuration; //追加  同時に参照設定System.Configurationの追加も必要
using System.Diagnostics;   //追加 FileVersionInfoのため
using System.Reflection;    //追加  assemblyのため
using System.Threading;
using System.Net;
using System.Net.Sockets;
using NLog;

namespace NFC_card_reader
{
    //https://dev.10yro.co.jp/entry/2022/10/18/195603
    public partial class Form1 : Form
    {
        private const string CardReaderName = "Sony FeliCa Port/PaSoRi 3.0 0";

        public static ISCardContext context = null;

        private ISCardMonitor monitor = null;

        private delegate void AddListBoxDelegate(string cardId);

        public static Logger logger = LogManager.GetCurrentClassLogger();

        public static string apps_version_info;
        public static string LOCALPATH_URL = "/";

        public static long th_number;
        private Thread hMain;
        public static int HTTP_PORT;

        //エラー情報
        public static string error_mess;

        public const int INT_OK_RESP = 0;
        public const int INT_ERR_RESP = -1;

        public const string STR_OK_RESP = "0";
        public const string STR_ERR_RESP = "-1";
        public const string STR_NOT_USERENTRY_ERR_RESP = "-2";
        public const string STR_FORMAT_ERR_RESP = "-3";
        public const string STR_NOT_ENTRY_ERR_RESP = "-4";
        public const string STR_ALREADY_USED_RESP = "-5";
        public const string STR_NOT_SUPPORT_ERR_RESP = "-6";
        public const string STR_PRM_VALUE_ERR_RESP = "-7";
        public const string STR_COMMAND_NOT_SUPPORT_RESP = "-8";
        public const string STR_COMMAND_PRM_LEN_MISS_RESP = "-9";
        public const string STR_NOT_SUPPORT_PRAMS_ERR_RESP = "-10";
        public const string STR_NO_DATA = "";


        public Form1()
        {
            InitializeComponent();
            this.Load += Form1_Load;
            string myapp_config_file = "apps.config";    //監視用ポート番号定義ファイル

            Assembly asm = Assembly.GetExecutingAssembly();
            FileVersionInfo file_version = FileVersionInfo.GetVersionInfo(asm.Location);
            apps_version_info = String.Format("V{0}L{1}", file_version.FileMajorPart.ToString("00"), file_version.FileMinorPart.ToString("00"));

            HTTP_PORT = 8082;   //デフォルト値設定
            th_number = 0;
            error_mess = "";
            System.Diagnostics.Process currentProcess = System.Diagnostics.Process.GetCurrentProcess();
            // リフレッシュしないとプロセスの各種情報が最新情報に更新されない
            currentProcess.Refresh();

            //AP用定義ファイルをチェックして情報を読み込む "apps.config"
            if (System.IO.File.Exists(myapp_config_file))
            {
                //configファイルの読み込み
                StreamReader sr = new StreamReader(myapp_config_file, Encoding.GetEncoding("Shift_JIS"));
                string s = sr.ReadToEnd();
                sr.Close();
                string[] myspps_config = s.Split(new string[] { "\r\n" }, StringSplitOptions.None);
                for (int i = 0; i < myspps_config.Length; i++)
                {
                    string[] mysppsconfig = myspps_config[i].Split(new string[] { "," }, StringSplitOptions.None);
                    if (mysppsconfig[0] == "HTTP_PORT")
                    { HTTP_PORT = int.Parse(mysppsconfig[1]); }
                    else if (mysppsconfig[0] == "LOCALPATH_URL")
                    { LOCALPATH_URL = mysppsconfig[1]; }
                }
            }
            else
            {
                MessageBox.Show("『" + myapp_config_file + "』は存在しません。");
                HTTP_PORT = 8082;

            }

            hMain = new Thread(new ThreadStart(httpserver_task));   //
            hMain.Start();  //
            logger.Info("httpserver_task start");



            //メインプログラムの開始
            logger.Info("NFC card Reader Start " + apps_version_info);  //ログ

        }

        private void Form1_Load(object sender, EventArgs e)
        {
            try
            {
                context = ContextFactory.Instance.Establish(SCardScope.System);
                var readerNames = context.GetReaders();
                if (readerNames == null || readerNames.Length == 0 ||
                    !readerNames.Any(v => v == CardReaderName))
                {
                    throw new Exception("対象のICカードリーダーが見つかりません。");
                }
            }
            catch (NoServiceException nsex)
            {
                logger.Info("カードエラー :"+nsex.InnerException);
                throw;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (this.button1.Text == "モニター開始")
            {
                this.button1.Text = "モニター停止";
                logger.Info("StartMonitor");
                this.StartMonitor();

               
            }
            else
            {
                this.button1.Text = "モニター開始";
                logger.Info("StopMonitor");
                this.StopMonitor();
            }
        }
        /// <summary>
        /// モニター開始
        /// </summary>
        public void StartMonitor()
        {
            this.monitor = MonitorFactory.Instance.Create(SCardScope.System);

            // イベント登録
            this.monitor.StatusChanged += Monitor_StatusChanged;

            // 開始
            this.monitor.Start(CardReaderName);
        }

        /// <summary>
        /// モニター停止
        /// </summary>
        public void StopMonitor()
        {
            if (this.monitor == null)
            {
                return;
            }

            this.monitor.Cancel();
            this.monitor.Dispose();
        }
        /// <summary>
        /// モニター状態変更時イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Monitor_StatusChanged(object sender, StatusChangeEventArgs e)
        {
            logger.Info("Status Changed New State: "+ e.NewState+", Last State :"+ e.LastState);
            //Debug.WriteLine($"Status Changed New State : {e.NewState}, Last State : {e.LastState}");

            if (e != null && e.NewState == SCRState.Present && e.LastState == SCRState.Empty)
            {
                // ID取得
                var cardId = ReadCardId();
                Invoke(new AddListBoxDelegate(this.AddListBox), cardId);
            }
        }

        /// <summary>
        /// ListBoxにカードのIDを追加
        /// </summary>
        /// <param name="cardId"></param>
        private void AddListBox(string cardId)
        {
            this.listBox1.Items.Add($"{cardId}");
        }

        /// <summary>
        /// カードのIDを読み取る
        /// </summary>
        /// <returns></returns>
        public static string ReadCardId()
        {
            string cardId = string.Empty;

            ICardReader reader = null;
            try
            {
                reader = context.ConnectReader(CardReaderName, SCardShareMode.Shared, SCardProtocol.Any);
            }
            catch (Exception)
            {
                // Exceptionを握りつぶす
            }

            if (reader == null)
            {
                return cardId;
            }

            // APDUコマンドの作成
            var apdu = new CommandApdu(IsoCase.Case2Short, reader.Protocol)
            {
                CLA = 0xFF,
                Instruction = InstructionCode.GetData,
                P1 = 0x00,
                P2 = 0x00,
                Le = 0 // We don't know the ID tag size
            };

            // 読み取りコマンド送信
            using (reader.Transaction(SCardReaderDisposition.Leave))
            {
                var sendPci = SCardPCI.GetPci(reader.Protocol);
                var receivePci = new SCardPCI(); // IO returned protocol control information.

                var receiveBuffer = new byte[256];
                var command = apdu.ToArray();

                var bytesReceived = reader.Transmit(
                    sendPci, // Protocol Control Information (T0, T1 or Raw)
                    command, // command APDU
                    command.Length,
                    receivePci, // returning Protocol Control Information
                    receiveBuffer,
                    receiveBuffer.Length); // data buffer

                var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case2Short, reader.Protocol);
                if (responseApdu.HasData)
                {
                    // バイナリ文字列の整形
                    StringBuilder id = new StringBuilder(BitConverter.ToString(responseApdu.GetData()));
                    cardId = id.ToString();
                }
                else
                {
                    logger.Info("ReadCardId:このカードではIDを取得できません");
                    cardId = "";
                }
            }

            reader.Dispose();

            return cardId;
        }
        public static string read_card_info()
        {
            long i = 0;
            long cnt = 0;
            string card_info = "";
            string card_info_tmp = "";
            logger.Info("ReadCard");
            //10秒間の間にカード情報を読み取る
            //読めなかったときは、ブランクで応答
            for (i = 0; i < 20; i++)
            {
                var cardId = ReadCardId();
                if (cardId == "")
                {
                    Thread.Sleep(500);  //500ms wait
                }
                else
                {
                    cnt += 1;
                    if (card_info == "" && card_info_tmp == "")
                    {
                        card_info = cardId;
                    }
                    else if (card_info != "" && card_info_tmp == "")
                    {
                        if (card_info == cardId)
                        {
                            logger.Info("Read Card info="+ cardId);
                            break;
                        }
                        else
                        {
                            card_info = cardId;
                        }
                    }

                }
            }
            if (i >= 20)
            {
                logger.Info("Card info not Read Time out");
            }
            return card_info;
        }
        //カード読み取り

        private void button2_Click(object sender, EventArgs e)
        {
            string card_info = "";
            card_info = read_card_info();
            if(card_info == "")
            {
                MessageBox.Show("Card情報が読み取れませんでした");

            }
            else
            {
                MessageBox.Show("Card info=" + card_info);

            }


        }
        private void httpserver_task()
        {
            try
            {
                //var port = Properties.Settings.Default.HTTP_SERVER_PORT;
                var port = HTTP_PORT;
                var httpserver = new HttpServer(port);
                httpserver.Listen();

                logger.Info("httpserver_task exit");
            }
            catch (Exception ee)
            {
                logger.Info("httpserver_task処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
        }
        private void StopHttpServer()
        {
            try
            {
                SendHttpServerStopMessage();


                var th_state = hMain.ThreadState.ToString();

                logger.Info("status=" + th_state);
                logger.Info("サーバアプリ終了しました。");
            }
            catch (Exception ee)
            {
                logger.Info("サーバアプリの終了処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
        }
        private void SendHttpServerStopMessage()
        {
            try
            {
                var port = HTTP_PORT;
                var sendMsg = "exit";

                using (var client = new WebClient())
                {
                    var res = client.UploadString($@"http://localhost:{port}", sendMsg);
                }
            }
            catch { }
        }
    }
}

簡易サーバ用のクラスです。HttpServer.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Web;
using System.Threading.Tasks;

//  sample http://localhost:8082/   resp 02-3D-D3-E1-17-60-00
namespace NFC_card_reader
{
    public class HttpServer
    {
        private Encoding EncodintType = Encoding.UTF8;
        private HttpListener Listener = null;
        private int Port = int.MinValue;
        private string Host;
        private bool flag = false;

        public HttpServer(int port = 80)
        {
            Port = port;
            Host = "*";
            Listener = new HttpListener();
        }

        public void Listen()
        {
            Listener.Prefixes.Clear();
            Listener.Prefixes.Add($@"http://localhost:{Port}/");
            Listener.Prefixes.Add($@"http://{Host}:{Port}/");
            Listener.Start();

            try
            {
                while (true)
                {
                    var context = Listener.GetContext();

                    if (flag) break;

                    try
                    {
                        var thread = new Thread(DoWork);
                        Form1.logger.Info("[httpListener]thread.Start");
                        thread.Start(context);
                        Form1.logger.Info("accept httpListener");
                    }
                    catch
                    {
                        var response = context.Response;
                        response.StatusCode = 500;
                        response.Close();
                    }
                }

                Form1.logger.Info("exit httpListener");
            }
            catch (Exception ee)
            {
                Form1.logger.Info("[httpListener]Listen処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
            finally
            {
                Listener.Stop();
                Listener.Close();
                Listener = null;
            }
        }

        private void DoWork(object obj)
        {
            Form1.th_number += 1;
            if (Form1.th_number > 10000)
                Form1.th_number = 1;

            long mythnum = Form1.th_number;
            string code_info = "";
            string label_info_uft8 = "";
            //string label_info_sjis = "";
            string mode_info = "";   // blank or test
            var context = (HttpListenerContext)obj;
            var request = context.Request;
            var response = context.Response;
            string request_type = request.HttpMethod;   // GETorPOST
            Form1.logger.Info(request.RawUrl);
            string card_info = "";
            try
            {
                // リクエストされたURLからファイルのパスを求める
                string path = request.RawUrl.Replace("/", "\\");
                string localpath = context.Request.Url.LocalPath;
                Form1.logger.Info("localpath=" + localpath);
                if (localpath == Form1.LOCALPATH_URL)
                {
                    //HttpUtility使うには、Microsoft.AspNet.WebApi nugetでインストール
                    var queryDictionary = HttpUtility.ParseQueryString(context.Request.Url.Query);
                    //code_info = queryDictionary.Get("code");
                    //label_info_uft8 = queryDictionary.Get("label");
                    Form1.logger.Info("label_info_uft8=" + label_info_uft8);
                    //まずはバイト配列に変換する
                    //byte[] bytesUTF8 = System.Text.Encoding.Default.GetBytes(label_info_uft8);

                    //バイト配列をUTF8の文字コードとしてStringに変換する
                    //label_info_sjis = System.Text.Encoding.UTF8.GetString(bytesUTF8);

                    mode_info = queryDictionary.Get("mode");

                    if (mode_info == "test")    //testのときは、ログ格納のみ実施
                    {
                        Form1.logger.Info("test mode");  //ログ
                    }
                    else
                    {
                        Form1.logger.Info("read card info");  //ログ
                        card_info = Form1.read_card_info();
                        Form1.logger.Info("card_info = " + card_info);


                    }
                }


                string resp_mess = "";

                try
                {
                    if (localpath != "/")
                    {
                        resp_mess = Form1.STR_OK_RESP + '\n';
                    }
                    else
                    {
                        resp_mess = card_info + '\n';
                    }

                    var sendBytes = EncodintType.GetBytes(resp_mess);

                    if (Form1.error_mess != "")
                    {
                        Form1.logger.Info("エラー情報:" + Form1.error_mess);
                    }


                    response.OutputStream.Write(sendBytes, 0, sendBytes.Length);
                    response.StatusCode = 200;
                    response.Close();
                }
                catch (Exception ee)
                {
                    Form1.logger.Info("DoWork処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");

                    resp_mess = Form1.STR_ERR_RESP + '\n';
                    byte[] sendBytes = EncodintType.GetBytes(resp_mess);
                    response.OutputStream.Write(sendBytes, 0, sendBytes.Length);
                    response.StatusCode = 500;
                    response.Close();
                }
            }
            finally
            {
                //Form1.logger.Info("DoWork処理が、終了しました。\n");
            }
        }
    }
}

C#のアプリを実行し、httpで要求を受信したときのログ情報(例)です。

2024-08-28 01:20:08.2974 INFO  [1] NFC_card_reader.Form1 - httpserver_task start 
2024-08-28 01:20:08.3393 INFO  [1] NFC_card_reader.Form1 - NFC card Reader Start V01L00 

以下は、カードを置いていないときのログ
2024-08-28 01:22:38.0873 INFO  [4] NFC_card_reader.Form1 - [httpListener]thread.Start 
2024-08-28 01:22:38.0883 INFO  [4] NFC_card_reader.Form1 - accept httpListener 
2024-08-28 01:22:38.2272 INFO  [5] NFC_card_reader.Form1 - / 
2024-08-28 01:22:38.2272 INFO  [5] NFC_card_reader.Form1 - localpath=/ 
2024-08-28 01:22:38.2292 INFO  [5] NFC_card_reader.Form1 - label_info_uft8= 
2024-08-28 01:22:38.2292 INFO  [5] NFC_card_reader.Form1 - read card info 
2024-08-28 01:22:38.2292 INFO  [5] NFC_card_reader.Form1 - ReadCard 
2024-08-28 01:22:48.5866 INFO  [5] NFC_card_reader.Form1 - Card info not Read Time out 
2024-08-28 01:22:48.5866 INFO  [5] NFC_card_reader.Form1 - card_info =  

以下は、カードを置いていたときのログ
2024-08-28 01:23:07.7342 INFO  [4] NFC_card_reader.Form1 - [httpListener]thread.Start 
2024-08-28 01:23:07.7342 INFO  [4] NFC_card_reader.Form1 - accept httpListener 
2024-08-28 01:23:07.7342 INFO  [6] NFC_card_reader.Form1 - / 
2024-08-28 01:23:07.7342 INFO  [6] NFC_card_reader.Form1 - localpath=/ 
2024-08-28 01:23:07.7342 INFO  [6] NFC_card_reader.Form1 - label_info_uft8= 
2024-08-28 01:23:07.7342 INFO  [6] NFC_card_reader.Form1 - read card info 
2024-08-28 01:23:07.7342 INFO  [6] NFC_card_reader.Form1 - ReadCard 
2024-08-28 01:23:07.7512 INFO  [6] NFC_card_reader.Form1 - Read Card info=02-3D-D3-XX-XX-XX-XX  実際の情報を加工しています。
2024-08-28 01:23:07.7512 INFO  [6] NFC_card_reader.Form1 - card_info = 02-3D-D3-XX-XX-XX-XX   実際の情報を加工しています。

PHP側のソースは、次回、紹介します。
今回は、PostManツールで実行しています。
http://192.168.13.130:8082/で要求を出すと
応答に02-3D-D3-XX-XX-XX-XXが返送されます。
カードによってはサイズも異なりますが、ユニークな情報なので
事前にDB上に保管し、照合処理すれば、認証処理や誰のアクセスかも判断できます。
自動ログイン処理や、タイムカードなどにも使えます。
スイカ、パスモ、運転免許証、クレジットカードなどたくさんのカードを使うことができるので、応用範囲もたくさんあると思います。

実際の操作ですが、スマホやPC上で、ICカードをリーダに置いたのち、読み込みボタンを押すと、画面からWebサーバに読み出しのリクエストが要求され、PHP言語で、curlコマンドを使って、カードリーダのあるWindowsOSにhttpでリクエストを出して、結果がブランクなら、読めなかった。ブランク以外なら電子カードの内容を読んでいるので、DBを検索して照合すれば、認証動作などが可能です。
WIndowsOSなども必要ですが、割と簡単にNFCカードの読み取りができます。最近では多くの非接触電子カードがあるので、専用にカードを準備しなくても、各自が持っているカードをシステムで読取り、DBに保存しておけば、誰がアクセスしているか判断できます。
次回の記事では、画面とPHPソースを紹介します。

この記事が気に入ったらサポートをしてみませんか?