まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:basic

サブジェクトが少しわかりにくいと思ったので最初にやりたいことを補足しておきます。

ウェブサービスを公開する際に Basic 認証と呼ばれる認証機能を有効にすることがあります。アクセス時にユーザーIDとパスワードが聞かれ、正しい組み合わせを入力しないと先に進めなくなる、というものです。会員制サービスや、正式公開前のサービスを限られた人だけで使いたい場合、グーグル等の検索エンジンクローラーに見つからない状態で運用したい場合などによく使われます:
thumb_basic


今回やりたかったのは、この Basic 認証を例えば以下の条件で実現するような Node.js アプリケーションを作ることです:
・パス /hello 以下にアクセスした際に Basic 認証が必要
・/hello にアクセスするには URL パラメータ id が必要(つまり GET /hello だけではエラーとなり、GET /hello?id=XXX というフォーマットでアクセスする必要がある)
/hello?id=XXX の時と /hello?id=YYY の時とでは Basic 認証のユーザーIDやパスワードが異なる


最後のが今回の肝となる条件です。パラメータ id の値ごとに Basic 認証のユーザーIDやパスワードが変わり(データベース等に格納されているものを id をキーに取り出して比較するイメージ)、これを Node.js + Express 環境でどのように実現するか、というのが挑戦内容です。


やりたいことが明確になったところで、改めて Node.js + Express 環境で Basic 認証をかける方法をググってみると、basic-auth-connect モジュールを使う方法がメジャーな方法の1つとして見つかります。これは簡単にいうと以下のような感じで Basic 認証をかけるものことができるものです:
var express = require( 'express' ),
    basicAuth = require( 'basic-auth-connect' ),
    app = express();

app.all( '/hello*', basicAuth( function( user, pass ){
  return ( 'username' === user && 'password' === pass );
}));

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :

GET /hello リクエストに対しては単に { status: true } という JSON を返すだけの定義がされていますが、その前に Basic 認証を有効にする部分が記述されています。この例では(/hello に何らかの URL パラメータが付属する場合も含めた) /hello* というパスに GET リクエストが行われた場合に Basic 認証が必要になり、ユーザーID 'username' 、パスワード 'password' が入力された場合のみ true(認証成功)で実際の GET /hello の処理が行われ、それ以外の場合は false(認証失敗)という扱いとなって再度入力が求められたり、何度か間違えると認証エラー扱いとなる、というものです。とても便利で、よく使っています。


さて、今回は上述の条件で Basic 認証を有効にする必要があり、少し異なる処理が必要です。正しいユーザーIDとパスワードは URL パラメータ id によって変わるのですが、この URL パラメータは req オブジェクから取り出す必要があり、今の形のままでは(認証判断時に req オブジェクトが取得できないので)取得が難しそうです。自分もこの basic-auth-connect モジュールを使う前提で実装を考えていたので詰まってしまいました。。

結論としては basic-auth-connect モジュールを使うことを諦め、自分で認証判断してエラー時にエラーコードを返す、という地味な処理に切り替えて実装できました:
var express = require( 'express' ),
    //basicAuth = require( 'basic-auth-connect' ),  //. basic-auth-connect は使わない
    app = express();

//. パラメータ id 毎に必要なユーザーIDとパスワード(本当はデータベース等から取得するイメージ)
var db = {
  "000" : { user: 'a', pass: 'x' },
  "001" : { user: 'b', pass: 'y' },
  "002" : { user: 'c', pass: 'z' }
};

/* URL パラメータ毎に認証情報を変えたい */
app.use( function( req, res, next ){
  //. hello* へのリクエスト時かどうかを判断
  var originalUrl = req.originalUrl;
  if( originalUrl.startsWith( '/hello' ) ){
    //. URL パラメータ ID を取り出す
    var id = req.query.id;
    //. 指定された ID のユーザー ID とパスワードが存在しているかどうかを調べる
    if( db[id] ){
      //. ヘッダから入力されたユーザーIDとパスワードを取り出す
      var b64auth = ( req.headers.authorization || '' ).split( ' ' )[1] || '';
      var [ user, pass ] = Buffer.from( b64auth, 'base64' ).toString().split( ':' );

      //. 入力内容が正しい場合のみ next() を返して本来の処理へ
      if( db[id].user == user && db[id].pass == pass ){
        return next();
      }else{
        //. 入力内容が間違っていた場合は認証エラー扱いとする
        res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
        res.status(401).send( 'Authentication required.' );
      }
    }else{
      //. 指定された ID が存在していなかった場合も認証エラー扱いとする
      res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
      res.status(401).send( 'Authentication required.' );
    }
  }else{
    return next();
  }
});

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :


赤字部分が今回作成した処理です。req オブジェクトからリクエスト先のパスや Basic 認証で指定された情報を取り出して正しい情報かどうかを判断し、正しい場合は本来の処理へ、間違っていた場合は HTTP の認証エラー結果を返すような内容を記述しています。basic-auth-connect モジュールを使うとこのあたりの細かな記述をする必要がなかったのですが、自分で判断する場合はこのあたりも自分の責任範囲で用意する必要があります。

上述の例では URL パラメータ id は "000", "001", "002" のいずれかである必要があり、それぞれの場合の Basic 認証情報(ユーザーID : パスワード)はそれぞれ "a":"x", "b":"y", "c":"z" としています。この正しい組み合わせが指定された場合のみ GET /hello が実行されて結果が返される、という処理が実行されるようになります。


(参照)
https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4


自分の「人生最初のプログラミング」は BASIC でした。

それも Visual Basic でなければ、N-88 BASIC でもありません。SHARP 製ポケットコンピュータに標準付属されていた BASIC(正式名称は知りません)で、一人用ブラックジャックなどを作って遊んでいたのでした。そんなわけで BASIC はそれなりの思い入れのあるプログラミング言語の一つです。

そしてヤフオクでたまたま見つけたファミリーベーシックを落札することができ、いつかこいつでプログラミング環境を・・・と思ったまま2年近く経ってしまいました。 (^^; この連休を使って改めてファミリーベーシックによるプログラミング環境を構築してみました。どれだけ需要があるか不明ですが(笑)、その経緯をまとめました。


まず「ファミリーベーシック」を知らない人向けの説明を。これは任天堂伝説の家庭用ゲーム機であるファミリーコンピュータ(以下、ファミコン)を使って動かすプログラミングソフトです。ファミコンは8ビットCPUを搭載した本体に ROM カセットと呼ばれるゲームカートリッジを差し込んで使うことで、様々なゲームソフトを楽しむことができました。2016 年には代表的な 30 本のゲームソフトを内蔵した「ファミコンミニ」が発売されて話題になりました。

ファミリーベーシックはそんなファミコンのソフトの1つで 1984 年(昭和 59 年)に発売されました。完成されたゲームそのものを楽しむというよりも、音楽を演奏したり、ビット絵を描いたり、、といった創作活動を楽しむソフトカートリッジでした。またコントローラーもファミコン付属のものを使うのではなく、ファミリーベーシックカートリッジに付属している外付けキーボードを接続して、キーボードによる入力がサポートされていました。このファミリーベーシックが持つ機能の1つが「BASIC 言語によるプログラミングと実行」でした。まだプログラミングが今ほど一般的でなかった時代にリリースされた、画期的すぎるソフトウェアでした。

そんなファミリーベーシックをヤフオクで(実行環境などをよく調べずに)落札し、しばらく経って実際にプログラミング環境を整えてみよう、と思い立ったのでした。普通に安いファミコン互換機(以下、FC互換機)を所有していたので、それを使って接続すればよい、と軽く考えていたのですが、いざ実際にパッケージを開けて接続してみようと思ったら意外な問題が発覚しました。



ファミリーベーシックは外付けキーボードを使うプログラミングソフトなのですが、この外付けキーボードはゲームコントローラーの代わりに本体に付けるものだとばかり思っていました(なのでファミコンのゲームコントローラーが使えるFC互換機であれば動かせると思っていました)。しかし実際には上記ツイートのようにエキスパンドコネクタと呼ばれる拡張インターフェースを使って接続する必要があるものだった、という事実が発覚したのでした。

自分が所有していたFC互換機を調べてみてもエキスパンドコネクタは付属していませんでした。ということは、少なくともこの時点で所有している機器だけではカセットを挿してソフトを起動することができてもキーボードを使って操作することはできない、ということです。それではプログラミング環境としてはあまりに貧弱なので、改めてエキスパンドコネクタ付きのFC互換機を探す必要がありました。

で、秋葉原に向かって、こういうのに詳しそうな「スーパーポテト」というお店へ行き、店長さんと相談しながら調べてみました。結論としては以下の2つの選択肢から選ぶ必要がありそうでした:
 ・互換機ではない「中古のファミコン」を購入するか、
 ・レトロフリークというFC互換機を、コントローラーアダプターセット付きで購入する

実はもう少し選択肢があるかと勝手に思い込んでいたのですが、エキスパンドコネクタ付属というのは思っていた以上に高い壁となりました。FC互換機で付属しているものは皆無で、唯一レトロフリークだけはオプション機器であるコントローラーアダプターセットがあればエキスパンドコネクタのインターフェースを持つこともできる、という状態でした。ちなみにFC互換機は安いものだと 2000 円前後で購入できるのですが、レトロフリークをコントローラーアダプター付きで購入すると 20000 円を超えます。出費額が完全に想定外。。。

ちなみにFC互換機でなく、本物のファミコンを中古で買う、という選択肢もありました(値段は少しだけこちらの方が安い)が、これだと AV 出力がアナログ、つまり昔のアナログ端子を持ったテレビでないと接続できません。一方のレトロフリークは HDMI 接続もできるという長所もあり、結論として、現在ファミリーベーシック環境を揃えるならレトロフリークが必要、ということになると思います。というわけでレトロフリークを購入しました:



こういってしまってはアレですが、昭和59年発売のゲームソフトを今の環境で再現する、という需要そのものが少ないと思っています。いますが、一方で「レトロフリークでファミリーベーシックを動かす」という環境での実現例を探すことができませんでした。「もしかしたら動かなかったりして・・」という不安に駆られつつも、実際に試せば済む話なので挑戦してみました(結論としては問題なく動きました)。

まずは普通(?)にレトロフリークを箱から取り出してセットアップします。本体にコントローラーを接続し、また HDMI ケーブルをモニターに接続して、それぞれの電源を ON にします。普通に起動しました。一般的なファミコンゲームソフトを動かすだけであれば、ゲーム ROM カセットをレトロフリークに差し込めば動くはず:
IMG_6241


さてここからがファミリーベーシックを使う場合のセットアップです。レトロフリーク付属のコントローラーアダプターがこれ。このアダプタにエキスパンドコネクタのインターフェースもついているので、こいつをレトロフリークに接続します:
IMG_6242


コントローラーアダプターをレトロフリークに接続し、そのエキスパンドコネクタにファミリーベーシックのキーボードを接続します。そしてファミリーベーシックのゲーム ROM も挿入:
IMG_6243


そして電源 ON ! おお!起動してるっぽい!! 
IMG_6245


そしてコントローラーのボタンを適当に押すと、なにやらメッセージが。「ワタシハファミリーコンピュータ デス」。ほうほう、そうですか。カタカナで聞かれるのがもう昔っぽい:
IMG_6248


そして「アナタハ ダレデスカ? ナマエヲ イレテクダサイ」と聞かれました。いよいよキーボード入力・・・ おお、普通に認識している!! レトロフリーク+HDMI 接続環境でもファミリーベーシックとそのキーボードはちゃんと動くようです:
IMG_6246


ちなみにこのキーボード、よく見ると "BS(Back Space)" キーがありません。間違えて入力した場合は矢印キーで戻って上書きしていくスタイルのようです:
IMG_6247


※ついでに書いておくと、このキーボードの F キーや J キーに他のキーと比べた特徴がなく、ブラインドタッチが非常に難しい、という難点がありました。。


そして約 30 年ぶりとなる古典 BASIC のプログラミング。当然スクリーンエディタ的な便利なものはなくて、プログラミングモードの画面に行番号を付けて入力していく感じ。とりあえず "HELLO WORLD."、まあ普通に動く動く: 
IMG_6251


なお、このソフトはプログラミングだけでなく、楽譜(音源)を入力して演奏させたり、日付を入力して(バイオリズム的な?)占いをすることもできます。世間では令和対応が間に合うとか間に合わないとか話題ですが、昭和59年製のこのソフトでは当然平成対応もしていません(笑):
IMG_6249


まあ表記以外は別にこれで困らないんだよな。。


今でもファミリーベーシックはヤフオクアマゾンで売られていることを見つけることがあって、入手困難というレベルではないと思っています。 ノスタルジーからつい買ってしまった人が現代の環境で実際に動かす際の参考になれば。



Node-RED を使うことで IoT データの収集や Web API の実装などが非常に簡単に実現できます。このブログでも何度か紹介していますし、公開されている外部モジュールを使って更にカスタム機能を追加することも可能です。

今回紹介するのは HTTP in ノードに認証機能を追加する node-red-contrib-httpauth ノードです。これを使うと Node-RED に標準装備されている HTTP in ノード(HTTP リクエストノード)に Basic 認証や Digest 認証を簡単に追加することができるようになります:
2018082900


実際に使う場合は、Node-RED の画面右上のメニューから「パレットの管理(Manage Pallette)」を選びます:
2018082901


設定ダイアログが表示されたら、"Palette" の "Install" タブで "httpauth" と検索します。すると node-red-contrib-httpauth ノードが見つかるので、"install" ボタンをクリックしてノードを追加します:
2018082902


インストールが成功すると以下のような表示になります。ここから実際にノードが使えるようになります:
2018082903


この時点でパレットにも "http auth" というノードが追加されていることが確認できます:
2018082904


実際に使う場合、http in ノードの直後に http auth ノードを配置します。この例では http in ノードの直後に http auth ノードを配置し、その後ろに(いつも使っているような)template ノードや function ノードを配置して、最後に http response ノードで HTTP リクエスト可能な API を作りました:
2018082905


template ノードの中身はシンプルにしています(認証が成功するとこの文字列が表示される、というテストです):
2018082906


そして http auth ノードに認証内容を設定します。この例では Basic 認証でレルム文字列は MyRealm 、そしてユーザー名 : user1 &パスワード : pass1 を設定しました。この状態でデプロイします:
2018082907


デプロイ後にウェブブラウザでこの API にアクセスすると、先程設定した http auth が機能し、指定した内容の認証が行われます。具体的にはユーザー名とパスワードを問い合わせるダイアログが表示され、先程指定した内容が入力されないと先へ進めません:
2018082908


上記で設定した内容(ユーザー名 : user1、パスワード : pass1)が正しく入力されると HTTP リクエストが正しく実行され、設定していた文字列が表示されます:
2018082909


この http auth ノードを使うことで、Node-RED で作成する API や Web ページに簡単に Basic 認証をかけることができそうです。


 

Tomcat やら Jetty やらといった Java アプリケーションコンテナ(Java アプリケーションサーバー)の種類に依存しない形でユーザー認証を実現するサンプル を作ってみました。実装にはフィルタを使います。また今回は認証の種類に Basic 認証を使っています。


まずは以下のような javax.servlet.Filter インターフェースの実装となるクラス: BasicAuthenticationFilter を作ります:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sun.xml.internal.messaging.saaj.packaging.mime.internet.MimeUtility;

public class BasicAuthenticationFilter implements Filter{
  //. レルム名
  private final String realmName = "myRealm";


  //. Filter の実装に必要なメソッド(何もしない)
  public void init( FilterConfig config ) throws ServletException{
  }
  public void destroy(){
  }

  //. フィルタリング処理の実装
  public void doFilter( ServletRequest req, ServletResponse res, FilterChain filterChain ) throws IOException, ServletException{
    ByteArrayInputStream bin = null;
    BufferedReader br = null;

    boolean isAuthorized = false; //. この値で認証の可否を判断する
    try{
      HttpServletRequest httpReq = ( HttpServletRequest )req;
      String basicAuthData = httpReq.getHeader( "authorization" );
      if( basicAuthData != null && basicAuthData.length() > 6 ){
        //. Basic認証から情報を取得
        String basicAuthBody = basicAuthData.substring( 6 ); //. 'Basic dG9tY2F0OnRvbWNhdA== ' 

        //. BASE64 デコード
        bin = new ByteArrayInputStream( basicAuthBody.getBytes() ); 
        br = new BufferedReader( new InputStreamReader( MimeUtility.decode( bin, "base64" ) ) );
        StringBuilder buf = new StringBuilder();
        String line = null;
        while ( ( line = br.readLine() )!=null ) {
          buf.append( line );
        }

        //. 入力された username と password を取り出す
        String[] loginInfo = buf.toString().split( ":" );
        String username = loginInfo[0];
        String password = loginInfo[1];
//.     System.out.println( "Basic " + username + ":" + password );

        //. 取り出した username と password で認証可否を判断する

        //. 実際にはここで LDAP やユーザー情報データベースと比較して判断することになる
        isAuthorized = true; //. 今回の例ではとりあえず何かが入力されていれば認証 OK とする
      }

      if( !isAuthorized ){
        //. (認証に何も指定されていなかった場合も含めて)認証 NG だった場合はブラウザに UnAuthorized エラー(401)を返す
        HttpServletResponse httpRes = ( HttpServletResponse )res;
        httpRes.setHeader( "WWW-Authenticate", "Basic realm=" + this.realmName );
        httpRes.setContentType( "text/html" );
        httpRes.sendError( HttpServletResponse.SC_UNAUTHORIZED ); //. 401

        //. 最初に認証なしでアクセスした場合はここを通るので、その結果ブラウザが認証ダイアログを出す、という流れ
      }else{
    	//. 認証 OK だった場合はそのまま処理を続ける
        filterChain.doFilter( req, res );
      }
    }catch( Exception e ){
      throw new ServletException( e );
    }finally{
      //. ストリームのクローズ
      try{
        if( bin!=null ) bin.close();
        if( br !=null ) br.close();
      }catch( Exception e ){
      }
    }
  }
}



また、web.xml の 内に以下の <filter> と <filter-mapping> の記述(青字部分)を追加します。この例では全ての URL (/*) に対して認証をかけるよう指定して います:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 

xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 

http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>BasicAuth</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

  :
  :

  <!-- Filter Configuration -->
  <filter>
    <filter-name>basicAuthFilter</filter-name>
    <filter-class>me.juge.basicauth.BasicAuthenticationFilter</filter-class>
  </filter>

  <!-- Filter Mapping -->
  <filter-mapping>
    <filter-name>basicAuthFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>



後は確認用の index.html として適当な内容のものを用意します:
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>ハローワールド!</h1>
</body>
</html>
↑超適当!


こうして作成した Java アプリケーションを動かして、ウェブブラウザからコンテキストルート(/) にアクセスすると、(初回は認証情報を付けずにアクセスする ので)作成したフィルタから 401 が返され、結果以下のような認証ダイアログが表示されるはずです:
2017031101


ここに適当な内容の文字列を入力して再度アクセスすると、(今度は何かが入っていたことになるので)上記サンプルでは認証 OK という判断になり、用意した index.html が表示される、という流れが実現できます:
2017031102



今回のサンプルはこちらに公開します:
https://github.com/dotnsf/BasicAuth












 

このページのトップヘ