社内業務システムのWebAPI実装を考えてみる

今まで仕事上、業務システムを開発してきましたが、ブラウザでアクセスするWebアプリケーションばかりでした。
しばらくはWebアプリ開発も続くでしょうが、ようやくタブレット端末を会社で活用する流れが出てきたので、
せっかく開発したWebアプリにタブレット端末からでもアクセスできるように、WebAPIの実装について考えてみました。

なおjQueryMobileなどを使ったWebアプリにする選択肢もありますが、ここはあえてJSON/JSONPを返すWebAPIの実装を考えます。

仕様

  • http/httpsアクセスできるものとし、レスポンスはJSON/JSONPで選択できる。
  • 業務システムなので、認証がある。
    • 認証は一度行ったらログアウトなどをしない限り継続する。
    • 認証部分はなるべく簡単に独自実装。
    • ログアウトも可能。
    • システムの利用ユーザーを変更する場合はログアウトして再ログイン。
  • エラーが発生した場合はHTTPステータスコードを返す。
  • クライアント非依存。


認証の考え方
認証は当然必要ですが、たとえばタブレット端末で利用する場合に、
Webアプリみたいに利用する度にログインが必要で、一定時間利用しないと(セッションタイムアウトにより)再ログインが必要・・・
というのはダルい気がするので、一度ログインしたらログイン情報がずっと継続されるように考えてみます。
(うちの会社では、端末のロック解除も数字4桁ではなくアルファベットと数字を混ぜて6文字以上必須というポリシーがあるので、これに加えてアプリでも認証が必要なのは面倒ではないかという考えです)
もちろん、ログアウトすることは可能としますし、ログイン成功した場合に発行されるトークンもログアウトすると破棄されるようにします。

アプリケーションサーバーのセッション機構を使っても同じことはできるかもしれませんが、
自分なりに細かいカスタマイズができたほうがいいかなと思い、独自実装することにします。

基本的な考え方としてはセッションIDのようなトークンを発行し、それをユーザー情報(データベース)に記録します。

  • ログイン成功時にトークンを発行し、以降はそのトークンを元にWebAPIにアクセスします。トークンは半角アルファベットと数字のランダムで30文字程度とします。
  • ログアウトするとトークンを破棄します。
  • 無効なトークンでアクセスを試みると、ステータスコード401を返します。

プログラム(SAStruts)の例
ここではトークンをログインキー(loginKey)としています。

  • WebAPI用Proxyクラス
public class LoginCheckForWebAPINSProxy implements ActionProxy
{
	@Resource
	protected HttpServletRequest request;
	
	@Resource
	protected HttpServletResponse response;
	
	@Resource
	protected UserService userService;
	
	@Override
	public String execute(ProxyChain proxyChain) throws Exception
	{
		String loginKey = request.getParameter("loginKey");
		User user = userService.findValidUserByLoginKey(loginKey);
		
		if(user == null)
		{
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			
			return null;
		}
		
		return proxyChain.invoke();
	}
}

リクエストパラメーター"loginKey"の値からデータベースのユーザー情報を検索します。
見つからない場合はステータスコード401を返します。
Proxyクラスについては、sastruts-extensionのActionProxyでログインチェック - おかひろの雑記を見て下さい。

  • ActionForm
	// ログインキー
	public String loginKey;
	
	// ログインID
	public String userId;
	
	// パスワード
	public String password;

	// レスポンスフォーマット json/jsonp
	public String format;
	
	// jsonpの場合のコールバック関数名
	public String callback;
	
	/**
	 * jsonかjsonpかを判断してレスポンスデータを生成する
	 * @param jsonData
	 * @return
	 */
	public String generateJsonResponseString(String jsonData)
	{
		if(this.format != null && this.format.equalsIgnoreCase("jsonp"))
		{
			if(this.callback == null || this.callback.equals(""))
			{
				this.callback = "callback";
			}
			
			return String.format("%s(%s);",this.callback,jsonData);
		}
		
		return jsonData;
	}

formatにより、jsonjsonpかを指定できるようにします。省略時はjsonです。
また、jsonpの場合はコールバック関数名も指定できます。
主にjsonpの場合にレスポンスデータを組み立てるメソッドを用意しています。

  • Action
	/**
	 * ログインチェック
	 * @return
	 */
	@Execute(validator=false,urlPattern="login/{userId}/{password}")
	public String login()
	{
		User user = userService.findByUserIdPassword(form.userId,form.password);
		
		if(user != null)
		{
			// 新しいログインキーを発行して更新
			user.loginKey = userService.generateLoginKey();
			userService.update(user);
			
			String responseJson = form.generateJsonResponseString(JSON.encode(user));

			response.setHeader("Access-Control-Allow-Origin","*");
			
			ResponseUtil.write(responseJson);
		}
		else
		{
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		}
		
		return null;
	}
	
	/**
	 * ログアウト
	 * @return
	 */
	@Proxy(proxy=LoginCheckForWebAPINSProxy.class,type=ProxyType.OVERRIDE)
	@Execute(validator=false,urlPattern="logout/{loginKey}")
	public String logout()
	{
		// ログインキーを消去
		user user = userService.findValidUserByLoginKey(form.loginKey);
		user.loginKey = null;
		userService.update(user);
		
		response.setStatus(HttpServletResponse.SC_OK);
		
		return null;
	}
	
	/**
	 * WebAPIで公開する機能
	 * @return
	 */
	@Proxy(proxy=LoginCheckForWebAPINSProxy.class,type=ProxyType.OVERRIDE)
	@Execute(validator=false,urlPattern="hoge/{loginKey}")
	public String hoge()
	{
		...
		
		String responseJson = form.generateJsonResponseString(JSON.encode(data));
		
		response.setHeader("Access-Control-Allow-Origin","*");

		ResponseUtil.write(responseJson);
		
		return null;
	}

ログイン処理では、指定されたユーザーIDとパスワードが正しいかどうかをチェックし、
正しければログインキーを発行してデータベースに保存、結果の情報をJSONにして返します。

ログアウト処理では、指定されたログインキーが正しいかどうかをチェックし、
正しければログインキーを破棄します。
破棄されるたログインキーは使えなくなるので、再ログインが必要になります。

WebAPIで公開する機能には、Proxyを設定します。(認証が必要ない機能は不要)

Access-Control-Allow-Originヘッダーは、主にAjaxでのクロスドメイン対応です。
jsonpもそうですが・・・)

まとめ
業務システムといってもいろいろあるので、認証の部分は求められるセキュリティ要件も変わってくると思います。
必要に応じて、トークンの有効期限を設けたり、IPアドレスやUser-Agentによるフィルターをしたり(おまけ程度かもしれませんが)するといいかと思います。

さっと考えついた程度のものですが、メモとして残しておきます。
もっと良い方法があれば、教えていただけたら嬉しいです。